> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/php-sdk.md).

# PHP SDK

A local policy engine that evaluates governance decisions inside your PHP process.

Fetches precompiled policy bundles from the Govplane Control Plane and evaluates every decision in-memory — no per-request network call.

### Requirements

* PHP 8.2+
* Extensions: `ext-curl`, `ext-json` (standard in all major PHP distributions).
* PHP-FPM

### Important: This SDK is designed to work with PHP-FPM.

The SDK is designed for **PHP-FPM workers** where the `RuntimeClient` instance is initialized once (e.g., via a DI container singleton or application bootstrap) and reused across many requests handled by the same worker. In that scenario:<br>

1. First request to a worker: `warmStart()` blocks until the bundle is fetched (one real HTTP call)
2. Subsequent requests: `refreshIfStale()` returns immediately (TTL not elapsed) → pure in-process evaluation
3. After `pollMs` elapses: one lightweight `HEAD` request to check ETag → usually no body download<br>

If you're using PHP in a setup where every HTTP request spawns a fresh process (true CGI, some serverless environments like AWS Lambda PHP runtimes), **you will pay a network round-trip on every single request** because `$this->cache` starts empty every time.&#x20;

There is no persistent shared memory between PHP-FPM workers either — each worker maintains its own independent in-memory cache.

### Installation

Add this to your project's composer.json:

```bash
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/GovPlane/php-runtime-sdk.git"
        }
    ],
    "require": {
        "govplane/runtime-sdk": "^0.1.0"
    }
}
```

Then run `composer install`

***

### Quick Start

```php
<?php
require_once __DIR__ . '/vendor/autoload.php';

use Govplane\Sdk\Client\RuntimeClient;
use Govplane\Sdk\Client\RuntimeClientConfig;
use Govplane\Sdk\Types\Decision\Allow as AllowDecision;
use Govplane\Sdk\Types\Decision\Deny as DenyDecision;
use Govplane\Sdk\Types\Decision\KillSwitch as KillSwitchDecision;
use Govplane\Sdk\Types\Decision\Throttle as ThrottleDecision;
use Govplane\Sdk\Types\Decision\Custom as CustomDecision;
use Govplane\Sdk\Types\Target;
use Govplane\Sdk\Types\TraceLevel;
use Govplane\Sdk\Types\TraceOptions;


$config = RuntimeClientConfig::builder(
    'https://<your-runtime-url>',
    'gprtk_<your-api-key>'
)->pollMs(5000)->build();

$client = new RuntimeClient($config);
$client->warmStart();

$target  = new Target('api', 'public', 'process-order');
$context = ['country' => 'UK'];

$result   = $client->evaluateWithTrace($target, $context, TraceOptions::forced(TraceLevel::Full));
$decision = $result->decision;

$type = match (true) {
    $decision instanceof AllowDecision      => 'allow',
    $decision instanceof DenyDecision       => 'deny (reason: ' . $decision->getReason() . ')',
    $decision instanceof KillSwitchDecision => 'kill_switch (service: ' . $decision->service . ')',
    $decision instanceof ThrottleDecision   => "throttle (limit: {$decision->limit}/{$decision->windowSeconds}s)",
    $decision instanceof CustomDecision     => "custom (value: {$decision->value})",
    default                                 => $decision->getDecision(),
};

echo "decision: {$type}\n";

$winner = $result->trace?->getWinner();
if ($winner !== null) {
    echo "winner: policy={$winner->policyKey} rule={$winner->ruleId}\n";
}

$client->flushTraces();
$client->stop();
```

### Decision Effects

| Effect        | PHP class             | Meaning                               |
| ------------- | --------------------- | ------------------------------------- |
| `allow`       | `Decision\Allow`      | Request permitted                     |
| `deny`        | `Decision\Deny`       | Request blocked                       |
| `kill_switch` | `Decision\KillSwitch` | Hard stop — overrides all other rules |
| `throttle`    | `Decision\Throttle`   | Rate-limiting metadata returned       |
| `custom`      | `Decision\Custom`     | Arbitrary JSON payload                |

**Precedence (highest → lowest):** `kill_switch → deny → throttle → allow → custom → deny-by-default`

### Configuration

```php
$config = RuntimeClientConfig::builder($baseUrl, $runtimeKey)
    // Polling
    ->pollMs(5_000)          // Normal poll interval (ms)
    ->burstPollMs(500)       // Poll interval during burst window
    ->burstDurationMs(30_000)
    // HTTP
    ->timeoutMs(5_000)
    ->userAgent('my-app/1.0')
    // Backoff
    ->backoffBaseMs(500)
    ->backoffMaxMs(30_000)
    ->backoffJitter(0.2)
    ->degradeAfterFailures(3)
    // Context validation
    ->validateContext(true)
    // Decision tracing
    ->traceDefaults(new TraceOptions(TraceLevel::Sampled, 0.01))
    ->onDecisionTrace($myTraceSink)
    // Incident controls
    ->incidentEnvFlag('GP_RUNTIME_INCIDENT')
    ->incidentFilePath('/run/govplane/incident.json')
    ->build();
```

| Method                                  | Default                           | Description                                      |
| --------------------------------------- | --------------------------------- | ------------------------------------------------ |
| `pollMs(long)`                          | `5000`                            | Normal bundle poll interval (ms)                 |
| `burstPollMs(long)`                     | `500`                             | Poll interval during burst window (ms)           |
| `burstDurationMs(long)`                 | `30000`                           | How long burst mode lasts after a change (ms)    |
| `timeoutMs(long)`                       | `5000`                            | HTTP request timeout (ms)                        |
| `userAgent(String)`                     | `govplane-runtime-sdk-java/0.1.0` | Custom User-Agent header                         |
| `backoffBaseMs(long)`                   | `500`                             | Exponential backoff starting delay (ms)          |
| `backoffMaxMs(long)`                    | `30000`                           | Maximum backoff cap (ms)                         |
| `backoffJitter(double)`                 | `0.2`                             | Jitter fraction applied to backoff               |
| `degradeAfterFailures(int)`             | `3`                               | Consecutive failures before `Degraded` status    |
| `validateContext(boolean)`              | `true`                            | Validate context map entries before evaluation   |
| `contextPolicy(ContextPolicy)`          | `null`                            | Allow/deny-list for context keys                 |
| `parseCustomEffect(boolean)`            | `false`                           | Parse `custom` effect payload into `parsedValue` |
| `traceDefaults(TraceOptions)`           | `null`                            | Default trace options for every evaluation       |
| `onDecisionTrace(TraceSink)`            | `null`                            | Synchronous callback for each trace event        |
| `onDecisionTraceAsync(TraceSinkAsync)`  | `null`                            | Async callback for trace events                  |
| `traceQueueMax(int)`                    | `1000`                            | Max trace events queued for async sink           |
| `traceDropPolicy(TraceQueueDropPolicy)` | `DROP_NEW`                        | What to drop when the queue is full              |
| `onTraceError(Consumer<Throwable>)`     | `null`                            | Error handler for failed async trace emissions   |
| `incidentEnvFlag(String)`               | `GP_RUNTIME_INCIDENT`             | Env var for runtime incident override            |
| `incidentFilePath(String)`              | `null`                            | Path to incident control file                    |
| `incidentFilePollMs(long)`              | `1000`                            | How often to poll the incident file (ms)         |

### PHP-Specific Notes

* **No background threads** — PHP does not have native threads. `evaluate()` calls `refreshIfStale()` inline (TTL-based). For long-running daemons/workers, call `$client->refreshIfStale()` explicitly in your event loop.
* **Async trace queue** — `TraceDispatcher` buffers trace events and drains synchronously on `flushTraces()`. Call it before process shutdown.
* **FPM / Shared state** — Each FPM worker has its own bundle cache. For large fleets, the bundle is re-fetched per worker after TTL expiry. Consider APCu for cross-worker sharing (not included in this SDK).

### Running Tests

```bash
cd php/gp-runtime-sdk
composer install
./vendor/bin/phpunit
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/php-sdk.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
