greghanton / simplifi-hq-oauth-api
OAuth2 client and request dispatcher for the Joy Pilot API tier
Package info
github.com/greghanton/simplifi-hq-oauth-api
pkg:composer/greghanton/simplifi-hq-oauth-api
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- guzzlehttp/guzzle: ^7.0
- guzzlehttp/promises: ^2.0
Requires (Dev)
- laravel/pint: ^1.29
- pestphp/pest: ^4.0.0
- phpstan/phpstan: ^2.0.0
- phpunit/phpunit: ^12.0.0
This package is auto-updated.
Last update: 2026-05-20 20:05:41 UTC
README
OAuth2 client and request dispatcher for the Joy Pilot API tier.
Install
composer require greghanton/simplifi-hq-oauth-api
Configuration
This package loads defaults from config.php and then reads values from environment variables using simplifiHqOauthApiLibEnv().
Environment variables
| Variable | Required | Default | Notes |
|---|---|---|---|
SIMPLIFI_API_GRANT_TYPE |
No | client_credentials |
Recommended for server-to-server. password is deprecated (OAuth 2.1 / RFC 9700 guidance). |
SIMPLIFI_API_CLIENT_ID |
Yes | - | OAuth client id. |
SIMPLIFI_API_CLIENT_SECRET |
Yes | - | OAuth client secret. |
SIMPLIFI_API_USERNAME |
Only for password grant |
- | Username/email for password grant flows. |
SIMPLIFI_API_PASSWORD |
Only for password grant |
- | Password for password grant flows. |
SIMPLIFI_API_SCOPE |
No | * |
OAuth scope. |
SIMPLIFI_API_URL_BASE |
Yes | - | API base URL, e.g. https://api.example.com/. |
SIMPLIFI_API_ACCESS_TOKEN_STORE_AS |
No | temp_file |
custom is recommended for production, temp_file is fine for local/dev. |
SIMPLIFI_API_ACCESS_TOKEN_TEMP_FILE_FILENAME |
No | Derived from project path/version | Only used when store_as=temp_file. |
SIMPLIFI_API_ACCESS_TOKEN_CUSTOM_KEY |
For custom store |
simplifi-hq-oauth-api-access-token |
Key passed to custom callables. |
SIMPLIFI_API_ACCESS_TOKEN_GET |
For custom store |
- | JSON-encoded callable. Signature: get($customKey): ?string. |
SIMPLIFI_API_ACCESS_TOKEN_SET |
For custom store |
- | JSON-encoded callable. Signature: set($customKey, $tokenJson): mixed. |
SIMPLIFI_API_ACCESS_TOKEN_DEL |
For custom store |
- | JSON-encoded callable. Signature: del($customKey): mixed. |
SIMPLIFI_API_ACCESS_TOKEN_LOCK |
Optional | - | JSON-encoded callable. Signature: lock($customKey, $ttlSeconds): bool. |
SIMPLIFI_API_ACCESS_TOKEN_UNLOCK |
Optional | - | JSON-encoded callable. Signature: unlock($customKey): mixed. |
SIMPLIFI_API_ERROR_LOG_FUNCTION |
No | "error_log" |
JSON-encoded callable for internal error logging. |
SIMPLIFI_API_DEFAULT_HEADERS |
No | [] |
JSON object of headers merged into each request. |
SIMPLIFI_API_SSL_VERIFY |
No | true |
TLS certificate verification for HTTP requests. |
SIMPLIFI_API_ADD_TRACE_DEBUG_HEADER |
No | false |
Adds trace-debug-header from caller backtrace. |
Basic usage
Synchronous request
<?php require __DIR__.'/vendor/autoload.php'; use SimplifiApi\ApiRequest; $response = ApiRequest::request([ 'method' => 'GET', 'url' => 'sales', ]); if (! $response->success()) { throw new RuntimeException($response->errorsToString()); } foreach ($response as $row) { // ApiResponse implements Iterator when response has a top-level "data" array. var_dump($row); }
Async request
<?php use SimplifiApi\ApiRequest; $promise = ApiRequest::requestAsync([ 'method' => 'GET', 'url' => 'sales', ]); $response = $promise->wait(); if ($response->success()) { var_dump($response->response()); }
Batch request
<?php use SimplifiApi\ApiRequest; $responses = ApiRequest::batch([ ['method' => 'GET', 'url' => 'sales'], ['method' => 'GET', 'url' => ['sales/$/invoice', 102]], ]); foreach ($responses as $response) { if (! $response->success()) { error_log($response->errorsToString()); } }
Access token storage
Supported modes:
custom(recommended for production; usually Redis)temp_file(unchanged local/dev fallback)
Redis setup via custom callables
SIMPLIFI_API_ACCESS_TOKEN_STORE_AS=custom SIMPLIFI_API_ACCESS_TOKEN_CUSTOM_KEY=simplifi-hq-oauth-api-access-token SIMPLIFI_API_ACCESS_TOKEN_GET="[\"\\\\App\\\\Cache\\\\TokenStore\", \"get\"]" SIMPLIFI_API_ACCESS_TOKEN_SET="[\"\\\\App\\\\Cache\\\\TokenStore\", \"set\"]" SIMPLIFI_API_ACCESS_TOKEN_DEL="[\"\\\\App\\\\Cache\\\\TokenStore\", \"del\"]"
Laravel example:
<?php namespace App\Cache; use Illuminate\Support\Facades\Cache; class TokenStore { public static function get(string $key): ?string { return Cache::store('redis')->get($key); } public static function set(string $key, string $value): bool { return Cache::store('redis')->forever($key, $value); } public static function del(string $key): bool { return Cache::store('redis')->forget($key); } }
Token refresh mutex (optional, recommended in production)
When several processes see an expired token at the same time, configure lock/unlock callables to prevent duplicate oauth/token refresh requests.
SIMPLIFI_API_ACCESS_TOKEN_LOCK="[\"\\\\App\\\\Cache\\\\TokenLock\", \"acquire\"]" SIMPLIFI_API_ACCESS_TOKEN_UNLOCK="[\"\\\\App\\\\Cache\\\\TokenLock\", \"release\"]"
Laravel lock example:
<?php namespace App\Cache; use Illuminate\Support\Facades\Cache; class TokenLock { private static array $locks = []; public static function acquire(string $customKey, int $ttl): bool { $lockName = $customKey.':oauth-refresh-lock'; $lock = Cache::store('redis')->lock($lockName, $ttl); if (! $lock->get()) { return false; } self::$locks[$lockName] = $lock; return true; } public static function release(string $customKey): void { $lockName = $customKey.':oauth-refresh-lock'; if (! isset(self::$locks[$lockName])) { return; } self::$locks[$lockName]->release(); unset(self::$locks[$lockName]); } }
Mutex flow inside the package:
- Try to lock (currently with ~10s TTL).
- If lock is acquired, refresh token and unlock.
- If lock is not acquired, wait 1-2s, re-check cache, and reuse token if present.
- Only fetch a fresh token if cache is still empty after waiting.
Event listener examples
Use listeners for metrics, tracing, or debugging without changing package internals.
<?php use SimplifiApi\ApiRequest; use SimplifiApi\ApiResponse; ApiRequest::addEventListener(ApiRequest::EVENT_BEFORE_REQUEST, function (array $requestOptions, array $config): void { // Example: emit request-start metric. }); ApiRequest::addEventListener(ApiRequest::EVENT_AFTER_REGULAR_REQUEST, function (ApiResponse $response): void { // Example: emit sync request timing metric. }); ApiRequest::addEventListener(ApiRequest::EVENT_AFTER_ASYNC_REQUEST, function (ApiResponse $response): void { // Example: emit async request metric. }); ApiRequest::addEventListener(ApiRequest::EVENT_AFTER_BATCH_REQUEST, function (ApiResponse $response): void { // Example: emit per-item batch metric. }); ApiResponse::addEventListener(ApiResponse::EVENT_RESPONSE_CREATED, function (ApiResponse $response): void { // Example: central hook after any response object is created. });
Laravel config:cache gotcha
src/helpers.php resolves env values in this order:
simplifiHqOauthApiEnv($key, $default)if your app defines it- fallback to Laravel
env($key, $default)if available - fallback default
With Laravel config:cache, runtime env() calls outside config files return null. To keep custom token store and mutex callables reliable under cached config, define simplifiHqOauthApiEnv() in your app and read from your own config layer instead of direct runtime env().
OAuth grant type default and safe rollout
Default grant type is now client_credentials.
client_credentialsis recommended for server-to-server usage.passwordremains supported but is deprecated.
To avoid breaking existing GUI environments that may have implicitly relied on the old default:
- In every consumer environment, set
SIMPLIFI_API_GRANT_TYPEexplicitly before upgrading. - Confirm the API tier Passport client is allowed to use
client_credentials. - Upgrade this package.
Development
composer run lint:check
composer run stan
composer run test