traffical/sdk is the official server-side SDK for PHP (^8.1). It fetches the config bundle, caches it (shared across PHP-FPM workers via a PSR-16 store), resolves parameters locally per request, and flushes events after the response is returned so experimentation never adds latency to the user-visible request.
It shares the language-agnostic Traffical SDK spec with the TypeScript and Swift SDKs — same SHA-256 v2 (UTF-8 byte) bucketing, same layered resolution engine, same contextual-bandit scoring — so a given unit buckets identically on every platform.
Installation
Initialization
ClientOptions is an immutable value object. Construct it with named arguments, or refine an existing instance with the fluent with*() methods (each returns a new instance).
Options
| Option | Type | Default | Description |
|---|---|---|---|
orgId | string | required | Organization ID |
projectId | string | required | Project ID |
env | string | required | Environment name ("production", "staging", etc.) |
apiKey | string | required | SDK key (traffical_sk_...) |
baseUrl | string | https://sdk.traffical.io | SDK API base URL |
localConfig | ConfigBundle | null | Bootstrap/offline bundle for instant cold-start |
refreshIntervalMs | int | 60000 | Cached bundle TTL |
evaluationMode | string | "bundle" | "bundle" (local) or "server" (delegate to the edge) |
assignmentLogger | callable|AssignmentLogger | null | BYO warehouse-native assignment logger |
disableCloudEvents | bool | false | Stop sending events to Traffical |
deduplicateAssignmentLogger | bool | true | Dedup logger calls per request |
eventBatchSize | int | 10 | Events per batch before auto-flushing |
httpClient, requestFactory, streamFactory | PSR-18/17 | discovered | HTTP seams |
cache | CacheInterface | null | PSR-16 shared store (FPM workers share one bundle) |
logger | LoggerInterface | NullLogger | PSR-3 logger |
clock | ClockInterface | system | PSR-20 clock |
plugins | Plugin[] | [] | Plugin list |
Resolving parameters
Context
Thecontext array is used for:
- Bucketing — the unit key (typically
userId, as configured on the project) drives which allocation the user gets. - Targeting — every field is available for policy condition evaluation. A policy with the condition
plan in ["pro", "enterprise"]only applies whencontext['plan']is one of those values.
Defaults
Always pass defaults for every parameter you read. They’re the fallback when no policy matches, when the bundle isn’t loaded yet, or when Traffical is unreachable — and they make your code’s intent explicit even when there’s no active experiment.Decisions vs getParams
getParams returns just the resolved values. decide returns a DecisionResult (assignments + a decisionId + metadata) — useful when you want to record an exposure only when the user actually sees the treatment, or pass the decision ID downstream.
Tracking events
Server mode
Delegate resolution to the edge worker instead of evaluating a local bundle. EachgetParams()/decide() performs (and caches per request) a POST /v1/resolve. Use it when caching a full bundle isn’t reasonable or you want zero client-side evaluation logic.
PHP lifecycle & event flushing
PHP’s request/response model differs from a long-lived Node process. On construction theClient registers a shutdown handler that:
- Calls
fastcgi_finish_request()if available, returning the buffered HTTP response to the client immediately. - Flushes queued decision/exposure/track events over PSR-18 HTTP — fire-and-forget, so transport errors are caught and logged via PSR-3 and never surface to the user.
eventBatchSize (default 10).
CLI, queue workers, and long-running processes
fastcgi_finish_request() doesn’t exist under the CLI SAPI or in long-lived workers. Flush explicitly at safe checkpoints:
$client->destroy() at shutdown, which runs plugin onDestroy hooks and flushes.
Sharing config across FPM workers
Each FPM worker is a separate process. Inject a PSR-16 shared cache (e.g.symfony/cache with APCu/Redis) so workers share one bundle rather than each refetching on its first request:
projectId:env and refreshes lazily on refreshIntervalMs. The same shared cache can back cross-request assignment-logger deduplication.
BYO warehouse-native assignment logging
Route structured assignment rows through your own pipeline (Segment, RudderStack, a DB, a queue) so assignment data never leaves your infrastructure. TheWarehouseNativeLogger helper maps each entry to a snake_case row, including the stable policy_key/allocation_key used for warehouse joins:
Plugins
Hook into the SDK lifecycle (onBeforeDecision, onDecision, onExposure, onTrack, onDestroy). Built-ins: DebugPlugin, DecisionTrackingPlugin, WarehouseNativeLoggerPlugin.
Framework integrations
Laravel
Auto-discovered
TrafficalServiceProvider + Traffical facade.Symfony
TrafficalBundle with a traffical config tree.OpenFeature
Optional
TrafficalProvider (requires open-feature/sdk).Error handling
The SDK never throws during resolution. If the bundle isn’t loaded yet — or never loaded successfully —getParams returns your defaults and decide returns the default assignments. Transport and refresh errors are caught and logged via the injected PSR-3 logger.
Next steps
A/B testing
Run a static experiment.
Warehouse-native
Compute metrics from your own data.
Canonical experiments
Patterns for backend, batch, and cross-surface tests.
API reference
The endpoints the SDK calls.