Easier PDF Creation using PHP with FileMaker Server 16

If you’ve been doing Custom Web Publishing with the FileMaker PHP API and needed to generate a PDF report you haven’t had many simple options. You can use one of the available PHP PDF classes such as TCPDF or FPDF which require specifying x and y coordinates, fonts and text to output in certain positions on the page, or setup a “robot” FileMaker Pro client that runs in a loop looking for new jobs to process.

Developers have been requesting the ability to generate PDF files server-side for many years, and from my conversations with FileMaker, Inc. engineers it was the number one feature request. FileMaker, Inc. were finally able to deliver on this with the release of the FileMaker 16 platform earlier this year, which included the following new features related to PDF and printing from FileMaker Server:

  • the Print script step now allows you to create PDFs in web browsers with FileMaker WebDirect
  • the Print Setup script step now allows you to specify PDF options for the Print script step and Save Records As PDF script step on FileMaker Server and FileMaker WebDirect
  • the Save Records as PDF script step now allows you to save PDFs with FileMaker Server and FileMaker WebDirect

If you look at the documentation for the Save Records as PDF script step you will see that is only compatible with FileMaker Server and FileMaker WebDirect  and not compatible with Custom Web Publishing (CWP), which is used by the PHP API when running FileMaker scripts. At this point you might be inclined to assume that there is nothing helpful in these changes for CWP/PHP developers, but there is a way to leverage the Save Records as PDF support under FileMaker Server from a PHP page.

The solution to this requires the Perform Script On Server script step (PSoS) which is CWP compatible. At this point it’s important to mention that PSoS does require the fmapp extended privilege to be enabled for your PHP/CWP users and their associated Privilege Sets. If your PHP users don’t normally access the database using a FileMaker Pro client then you may need to make some changes to your OnFirstWindowOpen script to limit their access (e.g. lock and hide the toolbar, navigate to a blank layout etc).

With some planning you can implement a solution that works like this:

  1. you have a PHP script – e.g. printReport.php – which includes a Print PDF Report button
  2. when a user clicks this button it performs a FileMaker script using the PHP API newPerformScriptCommand method. Let’s call this script Create PDF Report on Server
  3. the Create PDF Report on Server script is run as a CWP script (remember this script cannot create a PDF on its own). It includes a Perform Script On Server step which performs another script running under FileMaker Server which handles the Print Setup/Save Records as PDF steps and generates the PDF file and stores it in a container field (or passes the PDF back as Base64 encoded text using the Base64Encode function). Let’s call this script Save PDF
  4. the Create PDF Report on Server receives the script result from the Save PDF script – if you’re passing the PDF as Base64 encoded data you can set a container field using the Base64Decode function. Now that you have the PDF in a regular container field you can exit the FileMaker script and your printReport.php can use the getContainerData method to retrieve the PDF and either display this in the browser or download it

This might look complicated but I was able to implement this in a couple of hours for a client that needed to generate PDFs for a PHP/CWP solution. Before implementing this I would recommend you first become familiar with the server side printing/PDF changes in FileMaker Server 16 – a great place to start is the found in the App Innovations space of the FileMaker Community. Download the 05_Server side PDF support.fmp12.zip file which is part of DemoKit16 – a collection of tools aimed at helping demonstrate and explore new features of the FileMaker 16 Platform (brought to you by the World Wide Solutions Consulting team at FileMaker, Inc.). Upload this file to your FileMaker v16 Server and run through the examples and see how the Print Setup and Save Records as PDF script steps work under FileMaker Server 16. I ended up using these scripts:

  • [btn] How – Inventory Report – request report from server
  • [sbr] How – Inventory Report – get report as PDF (PSoS)

as the basis for my CWP scripts with only a few minor modifications, such as setting and retrieving various script parameters related to the records I was creating a PDF for.

The PHP code for this looks like this:

$scriptName = 'Request Invoice PDF'; 
$scriptParams = $orderID;  
$scriptObject = $fm->newPerformScriptCommand('WebOrders', $scriptName, $scriptParams); 
$scriptResult = $scriptObject->execute(); 
if(FileMaker::isError($scriptResult)) { $scriptError = 'Perform Script Error: '. $scriptResult->getMessage() . ' (' . $scriptResult->code . ')'; 
} else { 
$scriptError = ''; $record = $fm->getRecordById('WebOrders', $recid); 
$url = urlencode($record->getField('zv_Container_gr')); 
$fileName =  $orderID. ' Order Invoice.pdf'; 
$downloadLink = 'downloadFile.php?fileName='.$fileName.'&path='.$url; 
}

The downloadFile.php script simply retrieves the container data (PDF in this example) and does a force download of the PDF file to the user’s downloads folder. You could also change this to display the PDF in either the current tab or in a new tab as required.

Here’s a screenshot of the Request Invoice PDF script (have simplified this to show just the relevant steps for this technique):

and here’s the screenshot of the (simplified once again) Create Invoice PDF script that runs as the PSoS script:

I’m setting the generated PDF into a global container field in the Create Invoice PDF script as I don’t need to store the PDF file permanently in the database as this can be generated at any time, however you could also store the PDF in a regular container field in a new record etc as required and avoid having to Base64Decode/Encode the PDF data. It’s also worth remembering that you can’t use the PSoS script to set the PDF into a global field, as global fields are unique to each clients “session” so the CWP script and the PSoS script are each running in their own session, which is why you need to use the Base64Decode/Encode functions.

I’m looking forward to implementing this in existing CWP solutions which required complex FileMaker layouts to generate the PDF that weren’t easily created using the available PHP PDF classes. If you have any questions please leave a comment below.

Custom Web Publishing Changes in FileMaker Server v16

If you’re using Custom Web Publishing (CWP) with FileMaker Server v16 (PHP and XML) there are some changes in FileMaker Server v16 that you should be aware of that could break the functionality of your solution.

A client of ours recently upgraded their FileMaker Server to v16 which has a component that uses the FileMaker PHP API . One aspect of this involves a PHP page performing a FileMaker script which downloads files from an external service to the temp directory using the Get ( TemporaryPath ) function. Before I had a chance to troubleshoot this issue I noticed a post in the FileMaker Community titled “FMS 16 breaks CWP” – as a longtime PHP developer with many CWP client solutions this immediately caught my eye!

The post from David Jondreau mentioned that FileMaker Server v16 doesn’t support the Get ( TemporaryPath ) function – sure enough a quick check of the FileMaker v16 help confirms that under FileMaker v16 in “FileMaker WebDirect and Custom Web Publishing, this function is not supported and returns an empty string”. If you compare this to the FileMaker v15 help file it only states that in “In FileMaker WebDirect, this function is not supported and returns an empty string”.

I started troubleshooting my clients solution and after reviewing some server log table records that I had previously setup (always pays to have some kind of server side script logging setup that you can enable/disable on the fly as required) I could see that it was not generating a valid file path as the Get ( TemporaryPath ) function was returning an empty string. Our file paths to insert the image had gone from:

file://C:/Windows/Temp/S1494/IMG_4268.JPG

to:

file://IMG_4268.JPG

I was able to workaround this by splitting the single script into 2 scripts. The first script is called by the PHP page but it then utilises the Perform Script on Server (PSOS) script to run a script using FileMaker Server to download the file using the Get ( TemporaryPath ) function which is supported under FileMaker Server. The final sequence now looks like this:

  1. download.php file calls the newPerformScriptCommand to run a FileMaker script named ‘Start Image Download’ passing in a number of script parameters
  2. the ‘Start Image Download’ script runs under CWP and as part of this script it also includes a PSOS step to run another script named ‘Download Image’ in turn passing in a number of script parameters
  3. ‘Download Image’ runs as a PSOS successfully

I’ve since downloaded a copy of the FileMaker v16 help file and have been performing some searches to see if I can find any other changes for CWP compatibility. I couldn’t find any reference to these changes in the v16 Custom Web Publishing Guide or other release notes. David has started a new Product Idea in the FileMaker Community as well around this issue to see if some of the functionality can be restored. Hopefully someone from FileMaker Inc. can chime in with an update on why certain changes were made to CWP in v16 and provide a definitive list of what these changes were so we can put in place workarounds for solutions that will be deployed using FileMaker Server v16. If you know of any other CWP related changes in v16 please leave a comment below – I’ll update this post if I find out about any other changes as well.

Update 25 July, 2017: the FileMaker Community forum discussion has been updated with some feedback from FileMaker Inc. on why these changes were made: “Web clients (WebDirect, XML, & PHP) can no longer directly access files that are on the server (DBS) or worker (WPE) host file systems via scripting”. The solution is to use PSOS as described above.

 

Voice Calling with FileMaker

Since we released v1 of fmSMS many years ago we’ve been approached by customers from all around the world to work on integrations between FileMaker and other APIs (Application Programming Interfaces). Customers love using FileMaker to send and receive SMS/TXT messages, but they have also asked us to help them send faxes, MMS/Picture messages and to make outgoing voice calls from their FileMaker solution.

We’ve done a number of voice call integrations with FileMaker over the years – having FileMaker initiate a series of outgoing voice calls can be useful in a number of situations, including:

  • Alerts: you might not have mobile phone numbers for all your contacts/customers, but if you have a phone number you can call that number to provide an update to important events (e.g. school closures due to snow days)
  • Election Campaigns: we’ve helped one customer initiate a series of campaign calls to voters about upcoming elections (you may well have been on the receiving end of one of these)
  • Appointment Reminders: we helped one customer move away from having administration staff manually phone each customer about their upcoming appointments and having FileMaker generate these calls, saving them 1-2 hours of staff time each day

The process for generating a voice calling campaign works as follows:

  1. you’ll need to sign up for an account with one of the voice calling providers such as Twilio that handle the voice calling
  2. if you wish to have a dynamically generated message that references data from your FileMaker solution (e.g. to have the voice call say “Hello Andrew” instead of just “Hello”) you’ll need a process on your FileMaker Server than can generate the dynamic message for each contact to be called. We use the FileMaker API for PHP to generate these. If you don’t need a dynamic message you could simply hard code the message so it is the same for all recipients of the voice call
  3. a FileMaker script that is initiated either by a user or FileMaker Server that finds a group of records, then loops through each record and makes a request to the voice calling provider to generate a new outgoing call to that contact
  4. if you wish to be notified of the result of each call you can setup a webhook on your server to be notified by the voice calling provider each time a call is made with the result (engaged, answered etc) and the duration of the call

We’ve put together a simple demonstration video showing this in action – you can watch the video inline below or directly on YouTube:

 

If you would like to discuss integrating voice calling into your FileMaker solution please get in touch.

FileMaker PHP API Layout Tips

Over the past few months we’ve been working on a large FileMaker PHP Project for a client that required a web based interface to their FileMaker solution that worked on smartphones and tablets (both iOS and Android),  as well as the traditional desktops (Mac and Windows). The web interface was for users who were out of the office for large amounts of the week but needed to access the office solution from their car, client meetings or at home.

We’ve done many FileMaker PHP projects since the PHP API was first released in 2007 but this was by far our largest. Having worked closely with the PHP API for many months we’ve picked up a few tips and tricks along the way that we wanted to share, starting with Layouts.

If you’re not familiar with FileMaker’s PHP API the way the API interacts with your database is via FileMaker layouts – if you wish to find, edit, create or delete a record you need to specify the layout to use. Specifying the layout establishes the context for the query. It’s also important to note that everything on the layout is returned when performing a API command such as finding a record – this is why best practise has always been to only included the fields required by the PHP pages. The XML data that is returned by the API is quite verbose so you want to keep this to an absolute minimum as it can have significant performance implications if you don’t optimise your layouts for the API.

The following are some tips that you should be aware of when working with FileMaker Layouts and the PHP API:

Watch out for hidden fields not visible on the layout

In the screenshot below you can see there are only 5 fields on the ‘ContactsDetailsWeb’ layout: Title, First, Last, Initial and Name:

ContactsDetailsWeb

If we use the PHP API to request all the fields on that layout and echo this back to the browser:

$layout = $fm->getLayout('ContactsDetailsWeb');
// Get an associative array with the names of all fields as keys and FileMaker_Field objects as the array values
$layoutFields = $layout->getFields();
echo '<p><pre>'.print_r ($layoutFields).'</pre></p>';

we get the following:

Array(
 [Title] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Title[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [First] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => First[_autoEntered] => 1[_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [Last] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Last[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [Initial] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Initial[_autoEntered] => 1[_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => calculation[_valueList] => [_styleType] => [_maxCharacters] => 0)) [Name] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Name[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => calculation[_valueList] => [_styleType] => [_maxCharacters] => 0)) [Mobile] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Mobile[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [OfficeEmail] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => OfficeEmail[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [OfficePhone] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => OfficePhone[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [Fax] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => Fax[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [PersonalEmail] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => PersonalEmail[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0)) [PersonalPhone] => FileMaker_FieldObject([_impl] => FileMaker_Field_ImplementationObject([_layout] => FileMaker_LayoutObject([_impl] => FileMaker_Layout_ImplementationObject([_fm] => FileMaker_ImplementationObject([V73ee434e] => Array(
 [charset] => UTF - 8[locale] => en[logLevel] => 3[hostspec] => 127.0.0.1[recordClass] => FileMaker_Record[prevalidate] => [database] => ContactsStarterSolution[username] => Admin[password] =>
 ) [Vea4b3413] => [V9a3dcbce] =>) [_name] => ContactsDetailsWeb[_fields] => Array * RECURSION * [_relatedSets] => Array() [_valueLists] => Array() [Vab234ad8] => Array() [_database] => ContactsStarterSolution[_extended] =>)) [_name] => PersonalPhone[_autoEntered] => [_global] => [_maxRepeat] => 1[_validationMask] => 0[_validationRules] => Array() [_result] => text[_type] => normal[_valueList] => [_styleType] => [_maxCharacters] => 0))
 )

As you can see the result is quite verbose and includes some additional fields that you can’t see on the layout: Mobile, OfficeEmail, OfficePhone, Fax, PersonalEmail and PersonalPhone. So how does the PHP API see these fields? Let’s have a look at the same layout in Layout mode:

ContactsDetailsWeb Layout Mode

You can see these fields are hidden off to the right of the layout’s visible boundary and don’t appear in Browse mode using the FileMaker Pro client, but the PHP API sees all fields on the layout including the hidden fields. It’s advisable to not only have PHP specific layouts that contain just the fields you need for the PHP pages but don’t park/hide other fields off the edge of the layout. The PHP API will retrieve them as well and add additional overhead to your queries causing pages to take longer to load.

This only applies to those using FileMaker Pro/Server v12 or above – the ability to set a layout width was introduced in FileMaker Pro v12.

Beware of Layout Folder Names

We encountered an issue that caused quite a few hours of troubleshooting to resolve but seems obvious in hindsight. Here’s a screenshot showing a list of layouts in a FileMaker database:

List of Layouts

We had a PHP page that interacted with the ‘ContactsPHP’ layout – it would find records, edit records, create records etc and had been working fine for weeks without any issues when all of a sudden it stopped working one day. We ran multiple tests, double checked the spelling of the layout names, checked privileges etc but couldn’t understand what had happened.

We used the API to list out all the layouts in the database and echo this back to the browser:

$layout = $fm->getLayout('ContactsDetailsWeb');
$layouts = $fm->listLayouts();
echo '<p><pre>'.print_r ($layouts).'</pre></p>';

This returned the following array showing the ‘ContactsPHP’ layout:

Array(
 [0] => StartupScreen[1] => - [2] => Contacts[3] => ContactDetails[4] => Contacts | iPad[5] => ContactDetails | iPad[6] => Contacts | iPhone[7] => ContactDetails | iPhone[8] => Contacts | Web[9] => ContactDetails | Web[10] => ContactsDetailsWeb[11] => - [12] => Labels[13] => ContactList[14] => - [15] => ContactsPHP[16] => [17] => [18] => [19] => [20] => [21] => [22] => [23] => [24] => [25] =>
 )

Once again we couldn’t spot any issues with the layouts – the ‘ContactsPHP’ layout was amongst the list of layouts returned.

We then decided to rename the layout, update the PHP page and run some tests and it started working again. We renamed it back to ‘ContactsPHP’ and it failed immediately. Finally the penny dropped – we had a layout folder with exactly the same name that appear above the layout in the list of layouts. We tried renaming the layout folder and voila it started working again. It seems that for certain commands having a layout folder with the same name as the layout works fine, but there are times when it will cause the API to fail. We haven’t been able to isolate the specific circumstances yet but the safest approach is to simply avoid having layout folders with the same names as actual layouts entirely – remember that layout folders are special layout types and it appears that the PHP API can still interact with them when they share the same name as actual layouts.

We’ve run a number of tests and have been able to reproduce this many times on a sample file. We’ve found that if you’re checking for a 401 ‘no records found’ error with a find command like this:

if (FileMaker::isError($result)) {
    if ($result->code = 401) {
    $findError = $result->getMessage(). ' (' . $result->code . ')</p>';
    } else {
    $findError = $result->getMessage(). ' (' . $result->code . ')</p>';
    }
}

you will be mislead into thinking there was a real 401 ‘no records found’. If you examine the FileMaker_Error Object you should see it’s returning a 102 error:

[code] => 102

which is “Field is missing” and makes more sense if it’s using the Layout Folder instead of the actual Layout with the same name (we’ve also seen other instances where the API returns a misleading 401 error, such as when the XML returned is too large for the PHP XML  parser to handle – another reason to carefully optimise your PHP layouts). We first noticed this when we had a Layout Folder named ‘Websites’ and another layout used for a PHP find command called ‘WebSites’, but we have been able to reproduce this with other names as well.

Find Commands and the setResultLayout method

As we’ve discussed it’s ideal to optimise your FileMaker layouts used for the PHP API to only have the required fields on the layout necessary for the particular command, such as creating a new record or performing a find. It’s also possible to further optimise your queries to use separate layouts for the find command and the find results. For example you might have a form on your website that allows users to search for contact records by first name and last name only, however the PHP page is using a layout that has 25 fields from the Contacts table as you using the one Contacts layout for all PHP requests. For smaller record sets you can probably get away with this, but if you would like to optimise this further you can create a layout just for the find command with the 2 fields that you can search on and then have a separate layout that is used to display the find results, which might only require 6 fields for first name, last name, company, email, phone and mobile.

A typical PHP API find command that uses the one layout for both the find request and the find results looks like this:

$request = $fm->newFindCommand('ContactsDetailsWeb');
$request->addFindCriterion('FirstName', 'John');
$result = $request->execute();
if (FileMaker::isError($result)) {
    if ($result->code = 401) {
    $findError = 'There are no Records that match that request: '. ' (' . $result->code . ')';
    } else {
    $findError = 'Find Error: '. $result->getMessage(). ' (' . $result->code . ')';
    }
} else {
$records = $result->getRecords();
}

Here we are performing a find command on the ContactsDetailsWeb layout for any records that have a FirstName that equals ‘John’. We are then checking for any errors with the find command – if there are no matching records it will return a 401 error code, otherwise it will return another find error. If there is no error we are then retrieving all the found records.

To modify the find request to also use a different layout for the find results (to reduce the amount of XML data returned by the API by specifying a layout with just the fields required to show the search results) would look like this:

$request = $fm->newFindCommand('ContactsDetailsWeb');
$request->addFindCriterion('FirstName', 'John');
$request->setResultLayout( 'ContactsSearchResults' );
$result = $request->execute();
if (FileMaker::isError($result)) {
    if ($result->code = 401) {
    $findError = 'There are no Records that match that request: '. ' (' . $result->code . ')';
    } else {
    $findError = 'Find Error: '. $result->getMessage(). ' (' . $result->code . ')';
    }
} else {
$records = $result->getRecords();
}

The only difference between the 2 examples is the addition of this line that uses the setResultLayout method :

$request->setResultLayout( 'ContactsSearchResults' );

The setResultLayout method requests that the command’s result be returned in a layout different from the current layout. This certainly helps speed up the display of search results, however it does have one side effect. When you use the setResultLayout method as part of your find command and there are find errors (such as no records found) the API no longer returns a FileMaker_Error Object but instead returns a FileMaker_Result Object. This is a subtle but important difference – it means you can no longer simply trap for any find errors by calling:

if (FileMaker::isError($result)) {

as you will not get a FileMaker_Error Object. You can check for the number of matching records found, e.g.:

$found = $result->getFoundSetCount();

but it is certainly not as neat as simply checking for a FileMaker_Error Object.

I had trouble finding out more information about the setResultLayout method and ended up submitting a bug report with FileMaker Inc. The response they came back with suggests this is a known limitation when using the setResultLayout method. It would be great if this could be addressed in a future release of the PHP API.

(N.B. all references are to FileMaker Pro/FileMaker Server v14 which was the current shipping version at the time this article was written)

Andrew Duncan to Present at FileMaker DevCon 2014 Custom Web Publishing User Group

Databuzz is pleased to announce that Andrew Duncan will be presenting at the FileMaker DevCon 2014 Custom Web Publishing User Group in San Antonio, Texas. Andrew’s session will be on Surviving a Security Audit – how to best prepare for a security assessment of your FileMaker PHP web application. Andrew will demonstrate some of the top issues to be aware of and how to counter those and protect your FileMaker PHP web application.

The details for the 2014 Custom Web Publishing User Group meeting are:

DATE: Monday, July 28

TIME: 4:00 – 6:00 p.m. (before the 6:30 keynote)

ROOM: Iris

Check out the CWP User Group at FileMaker DevCon 2014 for more details – hope to see you there.

cwp_usergroup2014

 

The session slides and files are now available for download. If you have any questions about these please ask them in the comments below.

FileMaker API for PHP Valuelists and FileMaker Server v12

When working with the FileMaker API for PHP there are a number of functions that are helpful when working with Value Lists. You can use the listValueLists function  to return the names of any value lists associated with the  layout you are referencing, and the getValueLists function to return a multi-level associative array of value lists (i.e. the contents of the value lists on the layout).

I noticed something strange today when working on a FileMaker v12 file hosted with FileMaker Server v12 – using these 2 functions I was seeing value lists that I was sure were not on the layout. After a bit of testing I found the source of the problem – some of the fields which were set to display as a simple “Edit box” had previously been set to display as a “Drop-down list”, and it was the value lists that were previously associated with these fields that were showing. Simply changing the field’s control style from “Drop-down list” to “Edit box” was not enough to remove it from the XML that is returned by the Web Publishing Engine – if you’ve used FileMaker Pro for a while you’ll have encountered this feature where FileMaker can “remember” previous settings for a field on a layout and when you switch it back from “Edit box” to  “Drop-down list” it will remember the value list you had previously selected.

The only way to completely refresh the layout so that “remembered” value lists were not returned by the listValueLists and getValueLists functions was to remove the fields from the layout and manually add them again, ensuring that the control style was set to “Edit box” to start with (this is the default style when adding new fields to a layout).

I quickly checked a v11 solution running under FileMaker Server v11 and I wasn’t able to reproduce this issue, so it looks like a new issue with FileMaker Server v12 only. I’ll report it to FileMaker Inc in case it’s considered a bug, but it’s definitely a change from previous versions of FileMaker Server.

Simple Pagination with the FileMaker PHP API

During one of my sessions at the recent 2010 FileMaker Developer Conference in San Diego I was asked about the pagination/navigation on a search results page that is generated by the FileMaker PHP API. They were referring to the links you might see at the top/bottom of the search results page like this:

First | Previous | Record 21 – 40 of 52 Next | Last

which are generated dynamically and use the equivalent of conditional formatting in FileMaker to display only the necessary elements. For example on the first page of the search results there is no Previous page nor can you go to the First page as you are already on it; likewise with the last page there is no Next page and no Last page, so these should not appear depending on the context of the current page.

I finally found some time to put together a simple example of how you can generate these using the FileMaker PHP API.

As with FileMaker when you perform a search via PHP/Custom Web Publishing you will generate a found set of records (or no matching records). With a large found set of records you might want to present these to the user in chunks, for example you could present 5/10/20 records per page and allow the user to navigate to the next page. This is similar to how most search engines on the web work these days. Google for example defaults to showing 10 results per page. With the FileMaker PHP API you use the setRange() method to request only part of the found set of records by passing in 2 paremeters: the first parameter is the number of records to skip past and the second parameter is the maximum number of records to return.

Say you do a search and you find 52 matching records. Using a $max value of 20 you will get records 1-20 on the first page, records 21 to 40 on the second page, and records 41 to 52 on the third (and last) page. On the first page we are skipping 0 records, on the second page we are skipping 20 records and on the third (and last) page we are skipping 40 records.

Here’s an example of how to use the setRange method:


// Set a Max value. Paging 20 records at a time
$max = 20;
$skip = $_GET['skip'];
if(!isset($skip)) { $skip = 0; }
$request->setRange($skip, $max);

Here I’ve hardcoded the $max value to 20 which means I’ll only ever get back 20 records at the most per page. For the skip value I’m using the $skip variable and if this hasn’t already been set I’m setting it to 0, which is typically done on the first page of search results. Otherwise this will be passed as a parameter in the URL and the php script will check to see if this GET variable exists. Now we need to perform the find and check for a found set of records:

// Perform the Find
$result = $request->execute();
if (FileMaker::isError($result)) {
if ($result->code = 401) {
$errorMessage = "There are no Contacts that match that request: " . ' (' . $result->code . ')';
} else {
$errorMessage = "Contacts Find Error: " . $result->getMessage() . ' (' . $result->code . ')';
}
} else {

If there are matching records we then generate some variables that will be used in the First, Previous, Next and Last links, as well as calculating the number of records found and how many were “fetched” on the current page:

If you use the previous example of 52 matching records here are the values of these variables for each of the 3 pages of search results:

  • Page 1 (records 1-20): $skip = 0, $max = 20, $found = 52, $ fetchcount = 20, $prev = -20, $next = 20, $lastskip =42, $firstrecord = 1
  • Page 2 (records 1-20): $skip = 20, $max = 20, $found = 52, $ fetchcount = 20, $prev = 0, $next = 40, $lastskip =42, $firstrecord = 21
  • Page 3 (records 1-20): $skip = 40, $max = 20, $found = 52, $ fetchcount = 12, $prev = 20, $next = 60, $lastskip =42, $firstrecord = 41

I’m also using a pipe character as a separator between the links as well which is stored in the $sepbar variable. Now we’re ready to generate the links within the HTML part of the php page: Here’s the code for the “First” link:


<?php
if ($skip != 0) {echo '<a href="?skip=0">First</a>'.$sepbar;
}
?>

As we only want to show the First link on every page except for the first page of search results we are checking the value of the $skip variable. If $skip is not equal to 0 then we are not on the first page of search results so we can show this link, along with the pipe character to give it some space between the links.

For the “Previous” link we use:

<?php
if ($prev >= 0) {
echo '<a href="?skip='.$prev.'">Previous</a>'.$sepbar;
}
?>

Here we only want to show the Previous link if there is a previous page of search results, so we can check the contents of the $prev variable which will only equal 0 on the first page of the search results.

For the “Next” link we use:


<?php
if (($skip + $max) < $found) {
echo '<a href="?skip='.$next.'">Next</a>'.$sepbar;
}
?>

Here we need to check that there actually are subsequent pages of search results, which we do by checking that the total of the $skip and $max variables is less than the total found set of records.

Finally for the “Last” link we use:


<?php
if (($skip + $fetchcount) < $found) {
echo '<a href="?skip='.$lastskip.'">Last</a>';
}
?>

Here we need to test that we are not on the last page of the search results which we do by checking that the total of the $skip and $fetchcount variables is less than the total found set of records.

If you want to display the total number of found records along with the records that you are currently viewing (e.g. “Record 21 – 40 of 52”) you can use:


Record <?php echo $firstrecord; ?> - <?php echo $lastrecord; ?> of <?php echo $found; ?>

I’d like to credit Sonja Froyen’s article Custom Web Publishing: Paginate Your Results which I used as a starting point for some of these links (no need to reinvent the wheel here). If you Google “PHP Pagination” there are thousands of other articles on different ways to generate the pagination links, including variations which show how to include the search results page numbers like Google (e.g. “Previous 1 2 3 4 5 6 7 8 9 10 11 Next”. Most of these reference MySQL as the data source but it’s relatively easy to swap the references to the FileMaker PHP API.

As some of this code won’t make sense on it’s own here’s the full php page with the HTML (remember this is a simple page that finds all of the Contacts in the DevCon sample file):

<?php
require_once 'FileMaker.php';
require_once 'connections/INT002.php';

}
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>
FileMaker DevCon INT002 Contacts
</title>
<link rel="stylesheet" type="text/css" media="screen" href="css/glass_grey.css">
<style type="text/css">
form#auth label.error { display: none;
margin-top: 5px;
color: red;}
.appropriateError{
color: red;
}
.emphasise{
color: red;
}
</style>
</head>
<body>
<div id="container">
<!-- HEADER -->
<div id="header">
<h1>
FileMaker DevCon INT002 Contacts
</h1>
</div>
</div>
<table>
<thead>
<tr>
<th>
Customer ID                          </th>
<th>
First Name                            </th>
<th>
Last Name                            </th>
<th>
Country                            </th>
<th>
Phone Mobile                          </th>
</tr>
</thead>
<tbody>
<?php
$recnum = 1;
foreach($result->getRecords() as $contacts_search_row){
$rowclass = ($recnum % 2 == 0) ? "table_row" : "alt_row";
$recid = $contacts_search_row->getRecordId();
$pos = strpos($recid, "RID_!");
if ($pos !== false) {
$recid = substr($recid,0,5) . urlencode(substr($recid,strlen("RID_!")));
}
?>
<tr>
<td><a href='<?php echo "contactdetails.php?recid=$recid";?>'><?php echo nl2br( $contacts_search_row->getField('_kp_ContactID'))?></a></td>
<td>
<?php echo nl2br( $contacts_search_row->getField('NameFirst'))?>                            </td>
<td>
<?php echo nl2br( $contacts_search_row->getField('NameLast'))?>                            </td>
<td>
<?php echo nl2br( $contacts_search_row->getField('Country'))?>                            </td>
<td>
<?php echo nl2br( $contacts_search_row->getField('PhoneMobile'))?>                            </td>
</tr>
<?php $recnum++; } /* foreach record */?>
</tbody>
</table>
</div>
</div>
</body>
</html>

<!– Navigation Menu –>
<?php include_once ‘navigation.php’ ?><!– PAGE BODY –>
<div>
<?php
if ($errorMessage != ”) {
echo $errorMessage;
die;
}
?>
</div>
<div id=”content”>
<h1>
Contacts List                </h1>

Click on the Customer ID to view the full Contact details
<!–  Display record list page navigation controls –>
<div>

<?php
if ($skip != 0) {
echo ‘<a href=”?skip=0″>First</a>’.$sepbar;
}
?>

<?php
if ($prev >= 0) {
echo ‘<a href=”?skip=’.$prev.'”>Previous</a>’.$sepbar;
}
?>

Record <?php echo $firstrecord; ?> – <?php echo $lastrecord; ?> of <?php echo $found; ?>

<?php
if (($skip + $max) < $found) {
echo ‘<a href=”?skip=’.$next.'”>Next</a>’.$sepbar;
}
?>

<?php
if (($skip + $fetchcount) < $found) {
echo ‘<a href=”?skip=’.$lastskip.'”>Last</a>’;
}
?>

$layouts = $fm->listLayouts();
if(FileMaker::isError($layouts)) {
// FileMaker PHP API Error — Alert User.
$errorMessage = “FileMaker PHP Error: ” . $layouts->getMessage();
} else {

// Find all Contact records
$request = $fm->newFindAllCommand(‘WebContacts’);

// Set a Max value. Paging 20 records at a time
$max = 20;

$skip = $_GET[‘skip’];
if(!isset($skip)) { $skip = 0; }
$request->setRange($skip, $max);

// Perform the Find
$result = $request->execute();

if (FileMaker::isError($result)) {
if ($result->code = 401) {
$errorMessage = “There are no Contacts that match that request: ”  . ‘ (‘ . $result->code . ‘)’;
} else {
$errorMessage = “Contacts Find Error: ” . $result->getMessage() . ‘ (‘ . $result->code . ‘)’;
}

} else {

// Get the found records and setup page navigation links
$records = $result->getRecords();
$found = $result->getFoundSetCount();
$fetchcount = $result->getFetchCount();

// $totalpages = ceil($found / $max);

$prev = $skip – $max;
$next = $skip + $max;
if(($skip + $max) > $found) {$next = $skip; }

$lastskip = $found – $max;

$firstrecord = $skip + 1;

if ($fetchcount == $max) {
$lastrecord = (($firstrecord + $fetchcount) – 1);
} else {
$lastrecord = ($skip + $fetchcount);
}

$sepbar = ” | “;

// Get the found records and setup page navigation links
$records = $result->getRecords();
$found = $result->getFoundSetCount();
$fetchcount = $result->getFetchCount();

$totalpagesceil = ceil($found / $max);
$totalpagesfloor = floor($found / $max);

$prev = $skip – $max;
$next = $skip + $max;
if(($skip + $max) > $found) {$next = $skip; }

if($totalpagesceil == $totalpagesfloor) {
$lastskip = ($totalpagesceil – 1) * $max;
} else {
$lastskip = $totalpagesfloor * $max;
}

$firstrecord = $skip + 1;

if ($fetchcount == $max) {
$lastrecord = (($firstrecord + $fetchcount) – 1);
} else {
$lastrecord = ($skip + $fetchcount);
}

$sepbar = ” | “;

}
}

Databuzz eNews – July 2010 Released

We’ve just released our eNews newsletter for July 2010. You can view it online at:

http://www.databuzz.com.au/enews/enews_072010_generic.html

If you want to be added to the newsletter mailing list just leave a comment below and we’ll add you to the list until our new automated subscription service is operational.

FileMaker Server v11 – What’s New with the FileMaker API for PHP?

Since FileMaker Server v11 was released I’ve been wondering what new features there was as far as the API for PHP was concerned. There hasn’t been any mention of any new features for the API for PHP or the PHP Site Assistant. I did a quick comparison of the API for PHP files between FileMaker Server v10 and FileMaker Server v11:

FileMaker Server v10:

API Version Number 1.1
Minimum FM Server Version Number 10.0.0.0

FileMaker Server v11:

API Version Number 1.1
Minimum FM Server Version Number 10.0.0.0

So it looks like there’s no changes to the PHP API with FileMaker Server v11. I’ll run a diff on them just to be sure and report back if there are any changes.

FYI you can use the following PHP Code to get these details from the API:

php echo $fm->getAPIVersion(); ?>
php echo $fm->getMinServerVersion(); ?>

Databuzz eNews – December 2008 Released

We’ve just released our eNews newsletter for December 2008. You can view it online at:

http://www.databuzz.com.au/enews/enews_122008_generic.html

If you want to be added to the newsletter mailing list just leave a comment below and we’ll add you to the list until our new automated subscription service is operational.