confish / sdk
Official PHP SDK for confish — typed configuration, actions, and webhooks.
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.5
Requires (Dev)
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-05-01 00:11:53 UTC
README
Official PHP SDK for confish — typed configuration, actions, and webhook verification.
- One dependency (Guzzle)
- Typed exceptions and automatic retry on
429/5xx - Long-running action consumer with graceful-shutdown hook
- HMAC-SHA256 webhook verification (no extra deps)
Install
composer require confish/sdk
Requires PHP 8.1+.
Quick start
use Confish\Confish; $client = new Confish( envId: getenv('CONFISH_ENV_ID'), apiKey: getenv('CONFISH_API_KEY'), ); $config = $client->fetch(); echo $config['site_name'];
fetch, update, and replace return array<string, mixed>. PHPStan/Psalm users can document expected shapes via array shapes:
/** @var array{site_name: string, max_upload_mb: int, maintenance_mode: bool} $config */ $config = $client->fetch();
Reading and writing config
// GET /c/{env_id} $config = $client->fetch(); // PATCH — only listed fields change $client->update(['maintenance_mode' => true]); // PUT — replaces everything; omitted fields reset to defaults $client->replace([ 'site_name' => 'My App', 'max_upload_mb' => 50, 'maintenance_mode' => false, ]);
update and replace return the full updated configuration.
Write access must be enabled in environment settings before
updateandreplacewill work.
Logging
use Confish\LogLevel; $client->logger->info('Worker started', ['region' => 'eu-west-1']); $client->logger->error('Job failed', ['job_id' => 'abc']); // Or directly: $logId = $client->log(LogLevel::Critical, 'system down', ['code' => 503]);
Levels via the LogLevel enum: Debug, Info, Notice, Warning, Error, Critical, Alert.
Actions
The action consumer polls for pending actions, acknowledges them, runs your handler, and reports completion or failure — including idempotent skip if another consumer claimed the same action first.
use Confish\Action; use Confish\ActionUpdater; use Confish\Confish; use Confish\Exception\SkipActionException; $client = new Confish(envId: '...', apiKey: '...'); // Wire SIGTERM/SIGINT to a stop flag (requires ext-pcntl). $shouldStop = false; if (function_exists('pcntl_async_signals')) { pcntl_async_signals(true); pcntl_signal(SIGTERM, function () use (&$shouldStop) { $shouldStop = true; }); pcntl_signal(SIGINT, function () use (&$shouldStop) { $shouldStop = true; }); } $client->actions->consume( handler: function (Action $action, ActionUpdater $u): ?array { if ($action->type === 'place_order') { $u->update('Submitting order', ['params' => $action->params]); // ... do work ... return ['order_id' => 'abc123', 'filled_price' => 66980.0]; } throw new RuntimeException("unknown action type: {$action->type}"); }, pollInterval: 15.0, // base — defaults to 15s maxPollInterval: 60.0, // adaptive backoff cap shouldStop: fn (): bool => $shouldStop, onError: fn (Throwable $e, Action $a) => error_log("action {$a->id}: {$e->getMessage()}"), );
What happens automatically:
- A returned
arraybecomes the action'sresulton completion. - Throwing any exception fails the action with
['error' => $e->getMessage()]. - Throwing
SkipActionExceptionleaves the action acknowledged without resolving it. - A
409 Conflicton ack is silently skipped — safe to run multiple consumers. - The
shouldStopcallback is checked at the top of every poll; the loop exits cleanly. - After 3 consecutive empty polls the loop doubles its sleep up to
maxPollInterval, resetting topollIntervalthe moment any action is processed. Idle consumers make ~240 requests/hour by default.
You can also drive the lifecycle manually:
$actions = $client->actions->list(); $client->actions->ack('action_id'); $client->actions->update('action_id', 'progress', ['step' => 2]); $client->actions->complete('action_id', ['order_id' => 'abc']); $client->actions->fail('action_id', ['error' => 'timeout']);
Webhook verification
use Confish\Webhook; // Inside a controller / route handler: $body = file_get_contents('php://input'); // raw, unparsed $signature = $_SERVER['HTTP_X_CONFISH_SIGNATURE'] ?? null; if (! Webhook::verify( body: $body, signature: $signature, secret: getenv('CONFISH_WEBHOOK_SECRET'), )) { http_response_code(401); exit('Invalid signature'); } $payload = json_decode($body, true); // handle $payload['event'] ...
Laravel example:
use Confish\Webhook; use Illuminate\Http\Request; Route::post('/webhook', function (Request $request) { abort_unless( Webhook::verify( body: $request->getContent(), signature: $request->header('X-Confish-Signature'), secret: config('services.confish.webhook_secret'), ), 401, ); $payload = $request->json()->all(); // ... return response()->noContent(); });
verify uses constant-time comparison and rejects timestamps older than 5 minutes by default. Pass toleranceSeconds: 0 to disable. Always pass the raw, unparsed body — re-serializing parsed JSON breaks verification.
Errors
use Confish\Exception\{ AuthException, ConfishException, ConflictException, ForbiddenException, NetworkException, RateLimitException, ServerException, ValidationException, }; try { $client->fetch(); } catch (RateLimitException $e) { sleep($e->retryAfter ?? 1); } catch (ValidationException $e) { foreach ($e->errors as $field => $messages) { echo "$field: ".implode(', ', $messages); } } catch (ConfishException $e) { echo "HTTP {$e->statusCode}: {$e->getMessage()}"; }
By default the client retries 429 (honoring Retry-After) and 5xx responses up to twice. Tune with maxRetries on the constructor.
Options
use GuzzleHttp\Client as GuzzleClient; $client = new Confish( envId: 'a1b2c3d4e5f6', apiKey: 'confish_sk_...', baseUrl: Confish::DEFAULT_BASE_URL, // override for self-hosted httpClient: new GuzzleClient(['timeout' => 10.0]), // inject your own userAgent: 'my-app/1.0', maxRetries: 2, maxRetryDelay: 30.0, );
License
MIT