Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9cf6afa
Centralized `startCommunication` and `endCommunication` into Communic…
filiptorphage-mjuk Apr 30, 2026
699d468
Added retry mechanism on `callRestAPI` on authentication errors.
filiptorphage-mjuk Apr 30, 2026
311a768
Improved logic for setting inCommunicationScope in startCommunication…
filiptorphage-mjuk Apr 30, 2026
5b18b9f
Now uses the result of login on retrying
filiptorphage-mjuk Apr 30, 2026
43e1712
Clarified documentation on setRetryOnAccessTokenInvalidation
filiptorphage-mjuk Apr 30, 2026
c82c201
Replaced inCommunicationScope with keepAuth
filiptorphage-mjuk Apr 30, 2026
a415dee
Added minor documentation to the callRestAPI functions to clarify the…
filiptorphage-mjuk Apr 30, 2026
ab895f7
Added initial persistent session setup and session cache interface.
filiptorphage-mjuk May 5, 2026
fbf66ca
Adjusted logic of startCommunication and endCommunication to handle p…
filiptorphage-mjuk May 5, 2026
8e3b442
Made callRestAPIWithoutRetry private and made callRestAPI the only en…
filiptorphage-mjuk May 5, 2026
45c3445
Converted error code to int
filiptorphage-mjuk May 6, 2026
7e748bc
Made callRestAPIWithoutRetry protected and made TestProvider override it
filiptorphage-mjuk May 6, 2026
cceb0f4
Added comment to explain edge case of orphaned tokens
filiptorphage-mjuk May 6, 2026
73c72d9
Removed `PersistentSessionStore` and replaced it with `AbstractSessio…
filiptorphage-mjuk May 8, 2026
2c091d5
Added methods to override internal session cache properties
filiptorphage-mjuk May 8, 2026
bbc8020
Updated documentation to reflect that specifying a session automatica…
filiptorphage-mjuk May 8, 2026
1185f84
Cleared up documentation of startCommunication and endCommunication
filiptorphage-mjuk May 8, 2026
238e891
Clarified session cache behavior
filiptorphage-mjuk May 8, 2026
9fead75
Fixed the cache key generation
filiptorphage-mjuk May 8, 2026
f95432f
Added information that the APCu imlementation is not atomic
filiptorphage-mjuk May 8, 2026
57f4741
Added suggestions to composer.json
filiptorphage-mjuk May 8, 2026
720aea2
Added adapters for converting PSR-6 and PSR-16 cache implementations …
filiptorphage-mjuk May 8, 2026
39cb1af
Added resumeScopeAfterReauth property to start a communication sessio…
filiptorphage-mjuk May 8, 2026
1edfebf
Fixed setting cache key when no cache was provided
filiptorphage-mjuk May 8, 2026
be501ea
Corrected name of doc reference
filiptorphage-mjuk May 8, 2026
5e2d5e8
Added exceptions on trying to modify session cache properties when no…
filiptorphage-mjuk May 8, 2026
5a2b8f3
Now resumeScopeAfterReauth to false within endCommunication to not ac…
filiptorphage-mjuk May 11, 2026
a15fd28
Removed unused property
filiptorphage-mjuk May 11, 2026
ccd4acc
Now `resumeScopeAfterReath` is only used when a login attempt failed,…
filiptorphage-mjuk May 11, 2026
c4b0685
Fixed `resumeScopeAfterReauth` not being set if login() returned false
filiptorphage-mjuk May 11, 2026
e87bc8b
Better logic to set resumeScopeAfterReath
filiptorphage-mjuk May 11, 2026
d32d5c6
Removed PSR-6 and PSR-16 wrappers due to scope creep. Moved to a new …
filiptorphage-mjuk May 11, 2026
1a93949
Set implode separator to null byte (\0)
filiptorphage-mjuk May 11, 2026
4b60506
Removed misleading composer json suggestions of removed files.
filiptorphage-mjuk May 11, 2026
f33f01e
Fixed logic for setting keepAuth and resumeScopeAfterReauth
filiptorphage-mjuk May 12, 2026
79bd44e
Fixed so retrieving a cached session token now sets keepAuth when it …
filiptorphage-mjuk May 12, 2026
51d9f09
Fixed documentation of ApcuSessionCache
filiptorphage-mjuk May 12, 2026
a4d9665
Moved around logic in callRestAPI to make it clearer to read
filiptorphage-mjuk May 13, 2026
9c34cee
Improved clarity of ApcuSessionCache suggestion
filiptorphage-mjuk May 13, 2026
42f2e76
Enforced strict types declaration in ApcuSessionCache.
filiptorphage-mjuk May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"phpunit/phpunit": "*",
"phpstan/phpstan": "^2.0"
},
"suggest": {
"ext-apcu": "Optional: required only by the built-in ApcuSessionCache backend."
},
Comment thread
filiptorphage-mjuk marked this conversation as resolved.
"autoload": {
"psr-4": {
"INTERMediator\\FileMakerServer\\RESTAPI\\": "src/"
Expand Down
141 changes: 114 additions & 27 deletions src/FMDataAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace INTERMediator\FileMakerServer\RESTAPI;

use INTERMediator\FileMakerServer\RESTAPI\SessionCache\AbstractSessionCache;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\FileMakerLayout;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\FileMakerRelation;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\CommunicationProvider;
Expand Down Expand Up @@ -58,23 +59,33 @@ class FMDataAPI
* Ex. [{"database"=>"<databaseName>", "username"=>"<username>", "password"=>"<password>"}].
* If you use OAuth, "oAuthRequestId" and "oAuthIdentifier" keys have to be specified.
* @param boolean $isUnitTest If it's set to true, the communication provider just works locally.
*/
public function __construct(string $solution,
string $user,
string|null $password,
string|null $host = null,
int|null $port = null,
string|null $protocol = null,
array|null $fmDataSource = null,
bool $isUnitTest = false)
* @param AbstractSessionCache|null $sessionCache Cache backend for persistent sessions.
* If omitted, the library logs in and out on every database operation, or once
* per communication scope when using startCommunication() / endCommunication().
* If specified, session tokens are persisted and reused across requests via
* startCommunication() / endCommunication(), avoiding redundant logins against the FileMaker Server.
* When a session cache is specified, {@see self::setRetryOnAccessTokenInvalidation()} is
* automatically set to true, ensuring the library re-authenticates and retries the request if
* the cached token has expired on the FileMaker Server.
*/
public function __construct(string $solution,
string $user,
string|null $password,
string|null $host = null,
int|null $port = null,
string|null $protocol = null,
array|null $fmDataSource = null,
bool $isUnitTest = false,
AbstractSessionCache|null $sessionCache = null)
{
if (is_null($password)) {
$password = "password"; // For testing purpose.
}

if (!$isUnitTest) {
$this->provider = new Supporting\CommunicationProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource);
$this->provider = new Supporting\CommunicationProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource, $sessionCache);
} else {
$this->provider = new Supporting\TestProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource);
$this->provider = new Supporting\TestProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource, $sessionCache);
}
}

Expand Down Expand Up @@ -264,33 +275,42 @@ public function setThrowException(bool $value): void
}

/**
* Start a transaction which is a serial calling of multiple database operations before the single authentication.
* Usually most methods login and logout before/after the database operation, and so a little bit of time is going to
* take.
* The startCommunication() login and endCommunication() logout, and methods between them don't log in/out, and
* it can expect faster operations.
* Start a communication scope with a shared authenticated session.
*
* Usually most methods login and logout before and after each database operation.
* By calling startCommunication() and endCommunication(), methods between them don't
* log in and out every time, and it can expect faster operations.
*
* Without a session cache, one authenticated session is kept for the duration of
* the current communication scope and discarded when endCommunication() is called.
*
* With a session cache, the session token is persisted beyond the current communication
* scope and reused across requests. If no cached token is available, a new session is
* created and stored for future reuse.
*
* @throws Exception
*/
public function startCommunication(): void
{
try {
if ($this->provider->login()) {
$this->provider->keepAuth = true;
}
} catch (Exception $e) {
$this->provider->keepAuth = false;
throw $e;
}
$this->provider->startCommunication();
}

/**
* Finish a transaction which is a serial calling of any database operations, and logout.
* Finish a communication scope.
*
* Without a session cache, the authenticated session for the current communication
* scope is ended and the server session is logged out.
*
* With a session cache, the cached token's TTL is renewed if it still matches the
* token held by this instance. If another process has replaced the cached token in
* the meantime, only this instance's now-stale token is logged out, leaving the
* newer cached token intact.
*
* @throws Exception
*/
public function endCommunication(): void
{
$this->provider->keepAuth = false;
$this->provider->logout();
$this->provider->endCommunication();
}

/**
Expand Down Expand Up @@ -425,4 +445,71 @@ public function setExcludeTimeStampInException(bool $value = true): void
{
$this->provider->excludeTimeStampInException = $value;
}

/**
* Controls whether failed Data API calls are automatically retried after session invalidation.
*
* When enabled and a call fails with error 952 (invalid token) or 112 (window missing), the
* current session is discarded, a new session is established, and the call is retried once.
*
* When a session cache is provided to the constructor, retry on token invalidation is always
* active regardless of this setting. This flag only has an effect when no session cache is
* configured.
*
* Warning: The retry runs in a fresh session. Any session-scoped state from the original session
* is lost — for example, global fields set before the retry will not carry over.
* @param bool $value
*/
public function setRetryOnAccessTokenInvalidation(bool $value = true): void
{
$this->provider->retryOnAccessTokenInvalidation = $value;
}

/**
* Overrides the cache key used to store the FileMaker Data API session token.
*
* WARNING: Setting an incorrect cache key can cause session tokens to be shared
* across users or applications, which is a serious security risk. Do not use
* this method unless you fully understand the implications.
*
* The default cache key is a hashed representation of the following values:
* - The host name
* - The port number
* - The protocol (http or https)
* - The solution name
* - The user
*
* This default is sufficient for the vast majority of use cases. Only override
* this if you have a specific reason to do so.
* @param string $keyName The custom cache key name.
* @throws Exception If a session cache is not set, an exception is thrown.
*/
public function setSessionCacheKeyName(string $keyName): void
{
if ($this->provider->sessionCache === null) {
throw new Exception("setSessionCacheKeyName() requires a session cache to be configured via the constructor.");
}
$this->provider->sessionCache->setCacheKey($keyName);
}

/**
* Overrides the time-to-live (TTL) of the cached FileMaker Data API session token.
*
* WARNING: Setting a TTL that exceeds the FileMaker Data API session timeout (15 minutes)
* will cause the library to use expired tokens, resulting in authentication failures.
Comment thread
filiptorphage-mjuk marked this conversation as resolved.
* Do not use this method unless you fully understand the implications.
*
* The default TTL is 840 seconds (14 minutes), intentionally set one minute below the
* FileMaker Data API session timeout of 15 minutes to ensure the cached token is
* invalidated before it expires on the FileMaker Server.
* @param int $ttl Time-to-live in seconds. Defaults to 840 seconds (14 minutes).
* @throws Exception If a session cache is not set, an exception is thrown.
*/
public function setSessionCacheTtl(int $ttl = 840): void
{
if ($this->provider->sessionCache === null) {
throw new Exception("setSessionCacheTtl() requires a session cache to be configured via the constructor.");
}
$this->provider->sessionCache->setTtl($ttl);
}
Comment thread
filiptorphage-mjuk marked this conversation as resolved.
}
107 changes: 107 additions & 0 deletions src/SessionCache/AbstractSessionCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace INTERMediator\FileMakerServer\RESTAPI\SessionCache;

/**
* Base class for session cache implementations.
*
* Provides the cache key and TTL to concrete implementations, both of which
* are managed internally by the library. The cache key and TTL will not change
* during a single PHP request.
*
* As this cache stores FileMaker Data API session tokens, which are sensitive
* credentials granting full API access on behalf of the authenticated user,
* implementors must ensure that the underlying cache storage is secure and
* not accessible to unauthorized parties.
*
* To provide a custom cache backend, extend this class and implement
* {@see SessionCacheInterface::get()}, {@see SessionCacheInterface::set()},
* and {@see SessionCacheInterface::delete()}, using {@see self::$cacheKey}
* and {@see self::$ttl} in your implementations.
*
* Example:
*
* class RedisSessionCache extends AbstractSessionCache
* {
* public function get(): ?string
* {
* return $this->redis->get($this->cacheKey) ?? null;
* }
*
* public function set(string $value): bool
* {
* return $this->redis->setex($this->cacheKey, $this->ttl, $value);
* }
*
* public function delete(): bool
* {
* return $this->redis->del($this->cacheKey) > 0;
* }
* }
*
* @see SessionCacheInterface
*/
abstract class AbstractSessionCache implements SessionCacheInterface
{
/**
* The cache key for the current session.
*
* Always set by the library via {@see self::setCacheKey()} before any cache
* operation is performed. Will not change during a single PHP request.
* Implementing classes should use this property directly in their
* {@see SessionCacheInterface::get()}, {@see SessionCacheInterface::set()},
* and {@see SessionCacheInterface::delete()} implementations.
*/
protected string $cacheKey;

/**
* The time-to-live in seconds for cached session tokens.
*
* Set by the library via {@see self::setTtl()} before any cache operation
* is performed, defaulting to the value provided at construction time.
* Will not change during a single PHP request. Implementing classes should
* use this property directly in their {@see SessionCacheInterface::set()} implementation.
*
*/
protected int $ttl;

/**
* @param int $defaultTtl Default time-to-live in seconds for cached session tokens.
* Defaults to 840 seconds (14 minutes), reflecting the
* default FileMaker Data API session timeout. Adjust this
* value if your FileMaker Server is configured with a
* different session timeout.
*/
public function __construct(int $defaultTtl = 840)
{
$this->ttl = $defaultTtl;
}

/**
* Sets the cache key for the current session.
*
* This method is called internally by the library and should not be called
* manually. The key will not change during a single PHP request.
*
* @param string $key The cache key to use for subsequent cache operations.
*/
final public function setCacheKey(string $key): void
{
$this->cacheKey = $key;
}

/**
* Sets the time-to-live for cached session tokens.
*
* This method is called internally by the library and should not be called
* manually. The TTL will not change during a single PHP request.
*
* @param int $ttl Time-to-live in seconds for the cached session token.
*/
final public function setTtl(int $ttl): void
{
$this->ttl = $ttl;
}
}
85 changes: 85 additions & 0 deletions src/SessionCache/ApcuSessionCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace INTERMediator\FileMakerServer\RESTAPI\SessionCache;

use RuntimeException;

/**
* APCu-based session cache implementation.
*
* Caches FileMaker Data API session tokens using APCu, which stores data in
* shared memory on the server. Note that APCu cache is shared across all PHP
* processes on the same server, so cache keys must be sufficiently unique to
* avoid collisions between different users or applications.
*
* APCu is not enabled by default on all PHP installations. This implementation
* is provided as a default for those that have APCu enabled. If APCu is not
* available, a custom extension of {@see AbstractSessionCache} should be used
* instead.
*
Comment thread
filiptorphage-mjuk marked this conversation as resolved.
* As this cache stores sensitive FileMaker Data API session tokens, APCu is
* only appropriate in environments where server memory access is properly
* restricted.
*
* Note that cache operations in this implementation are not atomic. While care
* has been taken to minimize the risk of race conditions, concurrent requests
* sharing the same cache key may occasionally result in redundant
* re-authentication against the FileMaker Server. This is considered an
* acceptable trade-off given the constraints of the current implementation.
*
* @package INTER-Mediator\FileMakerServer\RESTAPI\SessionCache
* @link https://github.com/msyk/FMDataAPI GitHub Repository
* @version 36
*/
class ApcuSessionCache extends AbstractSessionCache
{
/**
* ApcuSessionCache constructor.
* @throws RuntimeException If APCu is not available.
*/
public function __construct()
{
parent::__construct();
if (!function_exists('apcu_enabled') || !apcu_enabled()) {
throw new RuntimeException("APCu is required to use ApcuSessionCache.");
}
}

/**
* Retrieves the cached FileMaker Data API session token for the current session.
*
* @return string|null The cached session token, or null if no token exists
* for the current key.
*/
public function get(): string|null
{
$value = apcu_fetch($this->cacheKey, $success);
return $success && is_string($value) ? $value : null;
}

/**
* Persists a FileMaker Data API session token in APCu.
*
* @param string $value The FileMaker Data API session token to store.
* This is a sensitive credential and must be treated as such.
* @return bool True on success, false on failure.
*/
public function set(string $value): bool
{
return apcu_store($this->cacheKey, $value, $this->ttl);
}

/**
* Deletes the cached FileMaker Data API session token.
*
* Returns false both when the key does not exist and when deletion fails.
*
* @return bool True on success, false if the key did not exist or deletion failed.
*/
public function delete(): bool
{
return apcu_delete($this->cacheKey);
}
}
Loading