Connecting from the backend
Our backend application, in the Backend
resources folder, is a Symfony 4 web app. You can find out more about Symfony from the project website, but in short it’s a modern PHP development framework, on which several of the leading PHP projects, like Laravel, Drupal and Concrete (to name a few) are built.
To make it simple to get this project up and running the folder contains all of the dependencies required already in there, so if you’re familiar with Symfony there’s no need to run composer install
or yarn install
because everything’s already there. I’ve done this specifically for this workshop to save bandwidth at the point everyone wants to start this activity. This app is also in GitHub if you want to use it as a base for building something else.
First up we need to do a little configuration. Open the Backend
folder in your IDE (or text editor). Locate the file .env.local
and update the settings in there following the comments provided. If you’re working on this at home you’ll need to change the server to point to the FileMaker server you’ve uploaded the resource files to (see activity one where we prepared the files)
To get the app up and running:
- open a terminal window on your Mac (Applications > Utilities > Terminal) or PowerShell on Windows (type powershell in the search is the easiest way to locate it)
- change to the directory you saved the resources to, e.g.
cd C:\dapi\resources\Backend
- start the built-in web server with
php bin/console server:run
and hit enter - check that the app is up by directing your browser to the url given (most likely
http://127.0.0.1:8000
)
Using Guzzle
To communicate with the FileMaker Data API we’re going to use the Guzzle HTTP client. This is easier than using cURL natively as it abstracts away a lot of the minor details and gives us a nice simple to use API. For full details of Guzzle see its documentation.
At it’s simplest level a Guzzle request looks like this
$client = new Client();
$response = $client->request($method, $uri, $options);
In the FileMaker Data API context, it’s the $options
where we need to do all the work. For example a Guzzle request to log a user in would look something like this.
$client = new Client();
$options = [
'headers' => [
'Content-Type' => 'application/json'
],
'auth' => [
'username', 'password'
]
];
$response = $client->request('POST', 'https://your.server/fmi/data/v1/databases/your-database/sessions', $options);
If the credentials are correct then you’ll get a JSON response from the server which includes the token you’ll need for subsequent requests. To save that token you’ll need to json_decode
the response like this:
$content = json_decode($response->getBody()->getContents());
$token = $content->response->token;
To use that token and make a subsequent request, for example to create a new record you’d do something like this:
$body = [
'fieldData' => [
'Title' => 'Dr',
'First' => 'Frederic',
'Last' => 'Spoon',
'Company' => 'FileMaker Inc',
'Job Title' => 'Chief Cook and Bottle Wash'
]
];
$options = [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => sprintf('Bearer %s', $token),
],
'body' => json_encode($body)
];
$response = $client->request('POST', 'https://your.server/fmi/data/v1/databases/your-database/layouts/your-layout/records', $options);
Lots of this should look really familiar to what we’ve done in Postman, then in FileMaker, and then JavaScript – essentially all we’re doing is translating the same basic parameters and structures as we’ve used previously into a slightly different structure which works with the Guzzle API.
Activity
Now that the application is up and running and we’ve had an overview of how Guzzle works we need to make a number of changes to get connected to the FileMaker Data API.
- Head back to the
Backend
folder in your IDE - Locate the file
src/Service/FileMakerAPI.php
and open that - Complete the TODOs in
fetchToken()
andperformRequest()
- Open
src/Service/ChartService.php
- Complete the TODOs in
loadData()
Solution
An example of fetchToken
using Guzzle
/**
* @param array $params
*
* @throws Exception
*/
private function fetchToken(array $params)
{
$client = new Client();
try {
// Use a Guzzle request to access the FileMaker Data API setting the necessary
// headers and credentials
$options = [
'headers' => [
'Content-Type' => 'application/json'
],
'auth' => [
$params['username'], $params['password']
]
];
$response = $client->request('POST', $this->baseURI . 'sessions', $options);
// We get JSON back from the FileMaker Data API. The Guzzle response object stores
// that in 'contents' which can be accessed with $response->getBody()->getContents()
// we then need to json_decode that so that we can access the object in PHP
$content = json_decode($response->getBody()->getContents());
// The token is in response.token. We need to save that for future use. You could write it
// to a file, but because we want this to be user-specific one good place to store it is
// in the user's session
$this->saveToken($content->response->token);
} catch (Exception $exception) {
// We need to catch any exceptions which occur and try and provide more useful feedback
// The Data API uses standard HTTP responses (see https://httpstatuses.com/)
/** @var ClientException $exception */
if (404 == $exception->getResponse()->getStatusCode()) {
// 404 is 'not found' and that's what gets returned when the user credentials are wrong
throw new Exception($exception->getResponse()->getReasonPhrase(), 404);
}
// FileMaker is also really good at giving other error information if things go wrong. That's also
// in the body contents which we can get from the exception using
// $exception->getResponse()->getBody()->getContents()
$content = json_decode($exception->getResponse()->getBody()->getContents());
// Extract the message and throw an exception which we can use in the calling method
// to access information about what went wrong.
throw new Exception($content->messages[0]->message, $content->messages[0]->code);
} catch (GuzzleException $exception) {
throw new Exception('Unknown error', -1);
}
}
An example of performRequest
using Guzzle
/**
* @param string $method HTTP method to use for the request (e.g. GET, POST, PATCH etc)
* @param string $uri The endpoint to call - everything after the name of the database
* @param array $options Any data to send to the API with this call
*
* @return array
*
* @throws Exception
*/
public function performRequest(string $method, string $uri, array $options)
{
// Set the standard headers which are needed for every call. We need to set
// the Content-Type and the Authorization header. These are merged into the
// options passed in which allows them to be overridden by any requests which
// require different headers (e.g. container uploads)
$headers = [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => sprintf('Bearer %s', $this->retrieveToken()),
]
];
// Create a Guzzle client
$client = new Client();
try {
// Actually make the call with Guzzle. We pass through the method, we add the baseURI to the
// requested uri, then merge the default headers with the other cURL options have have been
// passed in to the request
$response = $client->request($method, $this->baseURI . $uri, array_merge($headers, $options));
// The JSON response from the Data API is in the body of the response which we can access
// using $response->getBody()->getContents()
$content = json_decode($response->getBody()->getContents(), true);
// Depending on exactly which endpoint we're calling we either want the data, or the whole response
// if there's a data set, send that, otherwise the whole response array.
return isset($content['response']['data']) ? $content['response']['data'] : $content['response'];
} catch (Exception $e) {
/** @var ClientException $e */
$content = json_decode($e->getResponse()->getBody()->getContents());
if (401 == $content->messages[0]->code) {
// no records found
return [];
}
// if the token has expired or is invalid then in theory 952 will come back
// but sometimes you get 105 missing layout (go figure), so try a token refresh
if (in_array($content->messages[0]->code, [105, 952]) && !$this->retried) {
// TODO (Activity six) come up with some way to try logging the user back in
}
throw new Exception($content->messages[0]->message, $content->messages[0]->code);
} catch (GuzzleException $e) {
throw new Exception('Unknown error', -1);
}
}