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:
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:
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:
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)