Skip to content

Commit

Permalink
Merge pull request #39 from sib-retail/master
Browse files Browse the repository at this point in the history
Multiple enhancements
  • Loading branch information
thyyppa authored Mar 3, 2023
2 parents 77492bc + dcf4f8d commit bd30906
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 13 deletions.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
],
"require": {
"php": "^7.2",
"php": "^7.2 || ^8.0 || ^8.1",
"guzzlehttp/guzzle": "^7.0.1",
"ext-json": "*",
"ramsey/uuid": "^3.8 || ^4.0"
Expand All @@ -19,6 +19,9 @@
"phpunit/phpunit": "8.5.5",
"symfony/var-dumper": "^4.1"
},
"suggest": {
"ext-apcu": "*"
},
"autoload": {
"psr-4": {
"Hyyppa\\FluentFM\\": "src/"
Expand Down
44 changes: 40 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ Otherwise replace `localhost` with the server address.
use Hyyppa\FluentFM\Connection\FluentFMRepository;

$fm = new FluentFMRepository([
'file' => 'FilemakerFilename',
'host' => '127.0.0.1',
'user' => 'Admin',
'pass' => 'secret',
'file' => 'FilemakerFilename',
'host' => '127.0.0.1',
'user' => 'Admin',
'pass' => 'secret'
]);

// get a single record as array
Expand Down Expand Up @@ -178,6 +178,14 @@ $fm->find('customers')

---

#### Retrieving Metadata

To retrieve a Layout's metadata (columns types and properties)
```php
$fm->metadata('customers')
->exec();
```

#### Chainable commands

```php
Expand Down Expand Up @@ -251,6 +259,34 @@ $fm->find('customers')
->reset()
```
#### Persistent Authentication
If your PHP installation has the APCu module installed, this library is able to cache authentication tokens between requests.
Tokens are :
* associated to a filemaker host, database, user and password.
* valid for 15 minutes from time of creation or last use to query the Data API.
Everytime a token is used to make a new Data API request, its TTL (time to live) in the cache is prolonged by 14.5 minutes.
This mirrors Filemaker's mechanisms which extends the validity of tokens everytime they are used (with a maximum idle lifetime of 15 minutes).
If a token is invalidated while still stored in cache, the Exception is caught by the library and a single other attempt to authenticate and obtain a new token is done transparently by the library.
To define a custom TTL (and TTL extension duration), specify the `token_ttl` when instanciating a `FluentFMRepository` object.
```php
$fm = new FluentFMRepository([
'file' => 'FilemakerFilename',
'host' => '127.0.0.1',
'user' => 'Admin',
'pass' => 'secret',
'token_ttl' => 60, // shorter token cache TTL specified here
]);
```
**This feature should not be used on shared hostings or servers on which you cannot ensure that your APCu is strictly yours**
### Troubleshooting
#### Error: SSL certificate problem: unable to get local issuer certificate
Expand Down
126 changes: 122 additions & 4 deletions src/Connection/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
abstract class BaseConnection
{

/**
* @var Client
*/
Expand All @@ -25,7 +26,7 @@ abstract class BaseConnection
/**
* @var array
*/
protected $config;
protected $config = ['token_ttl'=>870];

/**
* @var string
Expand All @@ -47,7 +48,9 @@ abstract class BaseConnection
*/
public function __construct(array $config, Client $client = null)
{
$this->config = $config;
// we merge with the default config containing the newly introduced token cache ttl
// as to keep this new feature backward compatible
$this->config = array_merge($this->config, $config);

$options = [
'base_uri' => sprintf(
Expand Down Expand Up @@ -95,15 +98,28 @@ protected function authHeader(): array
}

/**
* Request api access token from server.
* Request api access token from server or from the cache if available.
*
* @return string
* @throws FilemakerException
* @throws ErrorException
*/
protected function getToken(): string
protected function getToken(?bool $require_new_authentication = false): string
{
// if we have a cached token available and we are not required to do a fresh authentication
if(
$this->isCacheAvailable() &&
$this->hasCachedToken() &&
!$require_new_authentication
) {
// prolong the life of this cached token (as the Data API extends the lifetime of tokens each time they are used)
$this->extendCachedTokenTtl();
// and retrieve it from the cache instead of hitting the API's authentication endpoint
return $this->token = $this->getCachedToken();
}

try {

$header = $this->client->post('sessions', [
'headers' => [
'Content-Type' => 'application/json',
Expand All @@ -115,6 +131,13 @@ protected function getToken(): string
throw new FilemakerException('Filemaker did not return an auth token. Is the server online?', 404);
}

// if we have a caching system available
if($this->isCacheAvailable()) {
// cache the token (it has an actual lifetime of 15 minutes,
// we cache it for only almost 15 minutes (-30 sec) by default)
$this->cacheToken($header[0]);
}

return $this->token = $header[0];
} catch (ClientException $e) {
throw new FilemakerException('Filemaker access unauthorized - please check your credentials', 401, $e);
Expand All @@ -130,4 +153,99 @@ protected function getToken(): string
throw $e;
}
}

/**
* Checks if we are able to use the APCu cache
*
* @return bool
*/
protected function isCacheAvailable() :bool {

return extension_loaded('apcu') && apcu_enabled();

}

/**
* Returns a token cache name, unique to this the current database and host
* It also includes a hash of the user and pass, in case multiple apps run on the same APCu
*
* @return string
*/
protected function getTokenCacheName() :string {

return 'filemaker-data-api-token-' . sha1(
$this->config('host') .
$this->config('file') .
$this->config('user') .
$this->config('pass')
);

}

/**
* Check if we have a non expired token for the current database and host
*
* @return bool
*/
protected function hasCachedToken() :bool {

return apcu_exists($this->getTokenCacheName());

}

/**
* Returns a cached token, supposedly valid for the current database and host
*
* @return string
*/
protected function getCachedToken() :string {

return apcu_fetch($this->getTokenCacheName());

}

/**
* Removes a cached token, useful when the Data API responds
* that it is invalid (ie. after a reboot of the filemaker instance)
*
* @return bool
*/
protected function removeCachedToken() :bool {

return apcu_delete($this->getTokenCacheName());

}

/**
* Stores a token for future use without the authentication step (avoiding an HTTP roundtrip)
*
* @return bool
*/
protected function cacheToken(string $token) :bool {

return apcu_store(
$this->getTokenCacheName(),
$token,
$this->config('token_ttl')
);

}

/**
* Extends the Time To Live of a cached token, to match Filemaker's
* own extension of the token validity when we use it
*
* @return void
*/
protected function extendCachedTokenTtl() :void {

// override the current token with the same one, but with a new ttl
apcu_store(
$this->getTokenCacheName(),
$this->getCachedToken(),
$this->config('token_ttl')
);

}

}
37 changes: 33 additions & 4 deletions src/Connection/FluentFMRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ public function records($layout, $id = null): FluentFM
return $this;
}

/**
* {@inheritdoc}
*/
public function metadata($layout): FluentFM
{
$this->callback = function () use ($layout) {
$response = $this->client->get(Url::metadata($layout), [
'Content-Type' => 'application/json',
'headers' => $this->authHeader(),
'query' => $this->queryString(),
]);

Response::check($response, $this->queryString());

return Response::metadata($response);
};

return $this;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -143,10 +163,10 @@ public function update(string $layout, array $fields = [], int $recordId = null)
$response = $this->client->patch(Url::records($layout, $id), [
'Content-Type' => 'application/json',
'headers' => $this->authHeader(),
'json' => ['fieldData' => array_filter($fields)],
'json' => ['fieldData' => $fields],
]);

Response::check($response, ['fieldData' => array_filter($fields)]);
Response::check($response, ['fieldData' => $fields]);
}

return true;
Expand Down Expand Up @@ -244,6 +264,12 @@ public function delete(string $layout, int $recordId = null): FluentFM
$this->callback = function () use ($layout, $recordId) {
$recordIds = $recordId ? [$recordId] : array_keys($this->find($layout)->get());

// if we haven't found anything to delete
if(!$recordIds) {
// don't attempt to delete anything, since that would fail
return true;
}

foreach ($recordIds as $id) {
$response = $this->client->delete(Url::records($layout, $id), [
'headers' => $this->authHeader(),
Expand Down Expand Up @@ -345,7 +371,7 @@ public function get()
$results = ($this->callback)();
} catch (\Exception $e) {
if ($e->getCode() === 401 || $e->getCode() === 952) {
$this->getToken();
$this->getToken(true);
$results = ($this->callback)();
} elseif ($e instanceof RequestException && $response = $e->getResponse()) {
Response::check($response, $this->queryString());
Expand Down Expand Up @@ -457,7 +483,10 @@ public function reset(): self
public function __destruct()
{
try {
$this->logout();
// only destroy the token on Filemaker if we don't have a caching mechanism available
if(!$this->isCacheAvailable()) {
$this->logout();
}
unset($this->client);
} catch (\Exception $e) {
}
Expand Down
18 changes: 18 additions & 0 deletions src/Connection/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ public static function records(ResponseInterface $response, bool $with_portals =
return $records;
}

/**
* Get response returned metadata.
*
* @param ResponseInterface $response
*
* @return array
*/
public static function metadata(ResponseInterface $response): array
{
$metas = [];

foreach (static::body($response)->response->fieldMetaData as $metadata) {
$metas[$metadata->name] = (array) $metadata;
}

return $metas;
}

/**
* Get response returned message.
*
Expand Down
11 changes: 11 additions & 0 deletions src/Connection/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public static function records(string $layout, int $id = null): string
return 'layouts/'.$layout.'/records'.$record;
}

/**
* @param string $layout
*
* @return string
*/
public static function metadata(string $layout): string
{

return 'layouts/'.$layout;
}

/**
* @param string $layout
*
Expand Down
9 changes: 9 additions & 0 deletions src/Contract/FluentFM.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ public function record($layout, $id): self;
*/
public function records($layout, $id = null): self;

/**
* Get metadata from filemaker table.
*
* @param $layout
*
* @return FluentFM
*/
public function metadata($layout): self;

/**
* Find records matching current query parameters.
*
Expand Down

0 comments on commit bd30906

Please sign in to comment.