confish/sdk

Official PHP SDK for confish — typed configuration, actions, and webhooks.

Maintainers

Package info

github.com/confishhq/confish-php

Homepage

pkg:composer/confish/sdk

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-04-30 23:24 UTC

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 update and replace will 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 array becomes the action's result on completion.
  • Throwing any exception fails the action with ['error' => $e->getMessage()].
  • Throwing SkipActionException leaves the action acknowledged without resolving it.
  • A 409 Conflict on ack is silently skipped — safe to run multiple consumers.
  • The shouldStop callback 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 to pollInterval the 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