Title: PHP SDK Locale: en URL: https://sensorswave.com/en/docs/data-integration/server-sdks/php/ Description: PHP SDK integration guide and API reference The Sensors Wave PHP SDK is a lightweight event tracking and A/B testing tool designed for PHP/FPM environments. It supports user event tracking, user property management, and full A/B testing capabilities. The SDK uses an offline runtime architecture that eliminates remote I/O on the request path, delegating all network communication to independent worker processes to ensure web request performance is unaffected. ## Core Features The PHP SDK provides the following core capabilities: - **Event Tracking**: Track user events with custom properties - **User Property Management**: Full user property operations including set, increment, append, and more - **A/B Testing Integration**: Support for Feature Gates, experiment variables, and dynamic configuration - **Automatic Impression Logging**: Automatically records A/B testing impression events - **Offline Runtime**: Zero remote I/O on the request path โ€” events are written to a local queue and A/B evaluations read from local snapshots - **Pluggable Adapters**: File-based adapters by default, with Redis and custom adapter support ## Requirements - PHP >= 8.2 - No external production dependencies ## Installation Install the SDK using Composer: ```bash composer require sensorswave/sdk-php ``` ## Runtime Architecture > [!NOTE] > > The PHP SDK's architecture differs from other language SDKs. Due to the PHP/FPM process model, the SDK separates synchronous request logic from asynchronous network I/O, ensuring web requests are never blocked. The SDK consists of three components: 1. **Client (Request Path)**: Runs within web requests, writes events to `EventQueue`, and reads A/B snapshots from `ABSpecStore` โ€” **makes no remote calls** 2. **sensorswave-sync Worker**: An independent process that fetches remote A/B metadata and saves snapshots locally 3. **sensorswave-send Worker**: An independent process that dequeues events and delivers them to the collector endpoint Default adapters use local file storage under `sys_get_temp_dir()`. The SDK also provides Redis adapters as reference implementations. For production environments, you should evaluate or implement your own `ABSpecStoreInterface` / `EventQueueInterface` based on your infrastructure needs. ```mermaid flowchart LR subgraph request["๐Ÿ‘ค Request Path"] Client([Client]) end subgraph sync["โฐ sensorswave-sync ยท Cron"] SyncCmd([SyncCommand]) end subgraph send["โฐ sensorswave-send ยท Cron"] SendCmd([SendCommand]) end ABStore[(๐Ÿ’พ ABSpecStore)] Queue[(๐Ÿ“จ EventQueue)] Meta["๐ŸŒ Meta EndpointPOST /ab/all4eval"] Collector["๐ŸŒ Collector EndpointPOST /in/track"] SyncCmd -- "fetch specs" --> Meta Meta -. "response" .-> SyncCmd SyncCmd -- "save()" --> ABStore ABStore -- "load()" --> Client Client -- "enqueue()" --> Queue Queue -- "dequeue()" --> SendCmd SendCmd -- "deliver" --> Collector Collector -. "ack / nack" .-> SendCmd classDef green fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#155724 classDef blue fill:#cce5ff,stroke:#004085,stroke-width:2px,color:#004085 classDef purple fill:#e2d9f3,stroke:#6f42c1,stroke-width:2px,color:#4a2d8a classDef orange fill:#fff3cd,stroke:#d68a00,stroke-width:2px,color:#856404 class Client green class SyncCmd,SendCmd blue class ABStore,Queue purple class Meta,Collector orange style request fill:none,stroke:#28a745,stroke-width:2px,stroke-dasharray:5 5 style sync fill:none,stroke:#004085,stroke-width:2px,stroke-dasharray:5 5 style send fill:none,stroke:#004085,stroke-width:2px,stroke-dasharray:5 5 ``` ## Quick Start ### Basic Event Tracking ```php trackEvent($user, 'PageView', [ 'page' => '/home', ]); // Close the client $client->close(); ``` ### Enable A/B Testing To enable A/B testing, provide an `ABConfig`: ```php getExperiment($user, 'my_experiment'); // Get parameters from the experiment result $btnColor = $result->getString('button_color', 'blue'); $showBanner = $result->getBool('show_banner', false); $discount = $result->getNumber('discount_percent', 0); echo "Button color: {$btnColor}, Show banner: " . ($showBanner ? 'yes' : 'no') . ", Discount: {$discount}%\n"; ``` ### Start Worker Processes The SDK requires background worker processes to handle network I/O. Schedule them via cron, Supervisor, or systemd: ```bash # Sync A/B metadata (required when A/B testing is enabled) ./vendor/bin/sensorswave-sync # Send event data ./vendor/bin/sensorswave-send ``` > [!WARNING] > > Worker processes are essential for the PHP SDK to function properly. Without `sensorswave-send`, events will accumulate in the local queue and never be delivered. Without `sensorswave-sync`, A/B tests will not receive the latest experiment configurations. --- ## User Type > [!WARNING] > > ### ๐Ÿ”‘ User Identity Requirements (MUST READ) > > **For ALL methods EXCEPT `identify`:** > > - โœ… At least one of `anonId` or `loginId` must be non-empty > - โšก **If both are provided, `loginId` takes priority for user identification** > > **For the `identify` method ONLY:** > > - โœ… **Both `anonId` AND `loginId` must be non-empty** > - ๐Ÿ”— This creates a `$Identify` event linking anonymous and authenticated identities ### Usage Examples **Creating users with different ID combinations:** ```php use SensorsWave\Model\User; // โœ… Valid: loginId only (for logged-in users) $user = new User(loginId: 'user-123'); // โœ… Valid: anonId only (for anonymous users) $user = new User(anonId: 'device-456'); // โœ… Valid: Both IDs (loginId takes priority for identification) $user = new User(anonId: 'device-456', loginId: 'user-123'); // โŒ Invalid: Neither ID provided โ€” this will FAIL $user = new User(); ``` **For the identify method โ€” both IDs are REQUIRED:** ```php // โœ… Correct: Both IDs provided $client->identify(new User( anonId: 'device-456', // โœ… Required loginId: 'user-123', // โœ… Required )); // โŒ Invalid: Only one ID โ€” identify will FAIL $client->identify(new User( loginId: 'user-123', // โŒ Missing anonId )); ``` **Adding A/B testing targeting properties:** ```php // Create a user $user = new User(anonId: 'device-456', loginId: 'user-123'); // Add A/B targeting properties (immutable pattern, returns a new instance) $user = $user->withAbUserProperty('$app_version', '11.0'); $user = $user->withAbUserProperty('is_premium', true); // Or add multiple properties at once $user = $user->withAbUserProperties([ '$app_version' => '11.0', 'is_premium' => true, ]); ``` --- ## Event Tracking ### identify โ€” Link User Identities Links an anonymous ID with a login ID (sign-up event). ```php $user = new User(anonId: 'anon-123', loginId: 'user-456'); $client->identify($user); ``` > **Note**: If `loginId` contains sensitive information (such as phone numbers, emails, or ID numbers), make sure to encrypt it before transmission to avoid exposing sensitive data in plain text. ### trackEvent โ€” Track Custom Events ```php $user = new User(anonId: 'anon-123', loginId: 'user-456'); $client->trackEvent($user, 'Purchase', [ 'product_id' => 'SKU-001', 'price' => 99.99, 'quantity' => 2, ]); ``` ### track โ€” Track with Full Event Structure ```php use SensorsWave\Model\Event; use SensorsWave\Model\Properties; $event = Event::create('anon-123', 'user-456', 'PageView') ->withProperties( Properties::create() ->set('page_name', '/home') ->set('referrer', 'google.com') ); $client->track($event); ``` --- ## User Property Management User properties describe user characteristics such as membership level, registration date, and preferences. Unlike event properties, user properties are persisted and associated with the user. ### profileSet โ€” Set User Properties Sets user properties; overwrites existing values. ```php $user = new User(anonId: 'anon-123', loginId: 'user-456'); $client->profileSet($user, [ 'name' => 'John Doe', 'level' => 5, ]); ``` ### profileSetOnce โ€” Set Only if Not Exists Sets user properties only if they do not already exist. ```php $client->profileSetOnce($user, [ 'first_login_date' => '2026-01-20', ]); ``` ### profileIncrement โ€” Increment Numeric Properties ```php $client->profileIncrement($user, [ 'login_count' => 1, 'points' => 100, ]); ``` ### profileAppend โ€” Append to List Properties ```php use SensorsWave\Model\ListProperties; $client->profileAppend($user, ListProperties::create()->set('tags', ['premium'])); ``` ### profileUnion โ€” Union List Properties ```php $client->profileUnion($user, ListProperties::create()->set('categories', ['sports'])); ``` ### profileUnset โ€” Remove Properties ```php $client->profileUnset($user, 'temp_field', 'old_field'); ``` ### profileDelete โ€” Delete User Properties ```php $client->profileDelete($user); ``` > **Note**: `profileDelete` deletes all user properties. This operation is irreversible. --- ## A/B Testing The PHP SDK includes full A/B testing support for evaluating Feature Gates, experiment variables, and dynamic configuration. A/B evaluations are performed locally from snapshots only โ€” no remote calls are made. If the snapshot is missing or stale, Feature Gate checks return `false` (fail closed). ### checkFeatureGate โ€” Check a Feature Gate Checks whether a Feature Gate (Boolean Toggle) is enabled. ```php $pass = $client->checkFeatureGate($user, 'new_feature_gate'); if ($pass) { // Feature is enabled enableNewFeature(); } else { // Feature is disabled useOldBehavior(); } ``` ### getFeatureConfig โ€” Get Feature Config Values Retrieves dynamic configuration values from a Feature Config. ```php $result = $client->getFeatureConfig($user, 'button_color_config'); // Get a string value (with default) $color = $result->getString('color', 'blue'); // Get a number value (with default) $size = $result->getNumber('size', 14.0); // Get a boolean value (with default) $enabled = $result->getBool('enabled', false); // Get an array value (with default) $items = $result->getSlice('items', []); // Get an associative array value (with default) $settings = $result->getMap('settings', []); ``` ### getExperiment โ€” Evaluate an Experiment Evaluates an experiment and retrieves variant parameters. ```php $result = $client->getExperiment($user, 'pricing_experiment'); // Get experiment variant parameter $pricingStrategy = $result->getString('strategy', 'original'); // Execute different logic based on variant switch ($pricingStrategy) { case 'original': showOriginalPricing(); break; case 'discount': $discount = (int) $result->getNumber('discount_percent', 0); showDiscountPricing($discount); break; case 'bundle': $bundleSize = (int) $result->getNumber('bundle_size', 1.0); showBundlePricing($bundleSize); break; default: showOriginalPricing(); } ``` ### evaluateAll โ€” Evaluate All Experiments Evaluates all loaded A/B specs at once and automatically records impression events. ```php $results = $client->evaluateAll($user); ``` ### Complete A/B Testing Example ```php withAbUserProperties([ '$app_version' => '11.0', 'is_premium' => true, ]); // Evaluate a Feature Gate if ($client->checkFeatureGate($user, 'new_ui')) { echo "New UI is enabled\n"; } // Evaluate an experiment $result = $client->getExperiment($user, 'pricing_test'); $variant = $result->getString('variant', 'control'); echo "Experiment variant: {$variant}\n"; // Get experiment parameters $discount = (int) $result->getNumber('discount', 0); echo "Discount: {$discount}%\n"; $client->close(); ``` --- ## Configuration Options ### Client Config | Field | Type | Default | Description | |-------|------|---------|-------------| | `trackUriPath` | `string` | `/in/track` | Event tracking endpoint path | | `flushIntervalMs` | `int` | `10000` | In-memory buffer flush interval (ms) | | `httpConcurrency` | `int` | `1` | Worker-side max concurrent HTTP requests | | `httpTimeoutMs` | `int` | `3000` | Worker-side HTTP request timeout (ms) | | `httpRetry` | `int` | `2` | Worker-side HTTP retry attempts | | `eventQueue` | `EventQueueInterface` | Local file queue | Event queue used by request-path tracking APIs | | `onTrackFailHandler` | `?callable` | `null` | Callback when event queue writes fail | | `ab` | `?ABConfig` | `null` | A/B testing configuration (disabled by default) | | `transport` | `?TransportInterface` | `null` | Worker-side custom HTTP transport | | `logger` | `?LoggerInterface` | Default logger | Custom logger implementation | > [!NOTE] > > The built-in default implementations for `eventQueue`, `logger`, and other fields are **reference implementations** intended for local development and quick validation only. For production environments, implement the corresponding interfaces based on your infrastructure needs. See [Custom Adapters](#custom-adapters) for details. ### ABConfig | Field | Type | Default | Description | |-------|------|---------|-------------| | `projectSecret` | `string` | `''` | A/B project secret for the sync worker (required) | | `metaEndpoint` | `string` | Main endpoint | Metadata server address (optional override) | | `metaUriPath` | `string` | `/ab/all4eval` | Metadata request path | | `metaLoadIntervalMs` | `int` | `60000` | Snapshot freshness threshold (minimum `30000` ms) | | `stickyHandler` | `?StickyHandlerInterface` | `null` | Custom sticky assignment handler | | `loadABSpecs` | `string` | `''` | Bootstrap snapshot from `getABSpecs()` cache | | `abSpecStore` | `ABSpecStoreInterface` | Local file store | Snapshot store used by the request path | > [!NOTE] > > The built-in default for `abSpecStore` is a **reference implementation**. For production environments, implement your own `ABSpecStoreInterface`. See [Custom Adapters](#custom-adapters) for details. --- ## Advanced: Caching A/B Specs To improve startup performance, you can cache the A/B specifications and load them upon client initialization. ```php // 1. Get specs from an initialized client $specs = $client->getABSpecs(); // 2. Save specs to persistent storage (e.g. file, database, Redis) // saveToStorage($specs); // 3. Load specs when creating a new client $savedSpecs = loadFromStorage(); $config = new Config( ab: new ABConfig( projectSecret: 'your-project-secret', loadABSpecs: $savedSpecs, // Inject cached specs ), ); // Client is immediately ready for A/B evaluation using cached specs $client = Client::create('https://your-endpoint.com', 'your-source-token', $config); ``` --- ## Built-in Adapters (Reference Implementations) The SDK ships with the following adapters as **reference implementations** for local development and quick validation: | Adapter | Description | |---------|-------------| | `LocalFileABSpecStore` | File-based A/B snapshot store (default) | | `LocalFileEventQueue` | File-based event queue (default) | | `RedisABSpecStore` | Redis-backed A/B snapshot store | | `RedisEventQueue` | Redis-backed event queue | Redis adapters depend on `RedisClientInterface`, allowing you to wire the SDK to your preferred Redis extension or client library without introducing a hard dependency. > **Note**: All built-in adapters (including both `LocalFile*` and `Redis*`) are **reference implementations** and are not guaranteed to meet all production requirements. Evaluate their suitability for your project and implement your own `ABSpecStoreInterface` / `EventQueueInterface` as needed. --- ## Custom Adapters For production environments, you need to implement `ABSpecStoreInterface` and `EventQueueInterface` based on your own infrastructure. The built-in adapter source code can serve as a reference. ### ABSpecStoreInterface Manages persistent storage for A/B snapshots. The sync worker calls `save()` to write, and the request-path client calls `load()` to read. ```php use SensorsWave\Contract\ABSpecStoreInterface; interface ABSpecStoreInterface { /** * Load the most recently saved snapshot JSON string. * Return null when no data is available โ€” the SDK will skip A/B evaluation. */ public function load(): ?string; /** * Persist the snapshot JSON string (called by the sync worker, never on the request path). */ public function save(string $snapshot): void; } ``` **Implementation notes:** - `load()` must return the exact string that was passed to `save()` โ€” do not re-encode or transform it - The implementation must be process-safe โ€” request-path FPM processes and the sync worker may read/write concurrently ### EventQueueInterface Manages event buffering between the request path and the send worker using a claim-based delivery model: ```mermaid flowchart LR enqueue["๐Ÿ“ฅ enqueue()"] --> pending["๐Ÿ“ฆ pending"] pending --> dequeue["๐Ÿ“ค dequeue()"] --> claimed["โš™๏ธ claimed(in-flight)"] claimed -- "โœ… ok" --> ack["โœ… ack()"] claimed -- "โŒ fail" --> nack["โ™ป๏ธ nack()"] --> pending classDef method fill:#87CEEB,stroke:#333,stroke-width:2px,color:darkblue classDef state fill:#E6E6FA,stroke:#333,stroke-width:2px,color:darkblue classDef success fill:#90EE90,stroke:#2E7D2E,stroke-width:2px,color:darkgreen classDef retry fill:#FFD700,stroke:#333,stroke-width:2px,color:black class enqueue,dequeue method class pending,claimed state class ack success class nack retry ``` ```php use SensorsWave\Contract\EventQueueInterface; use SensorsWave\Storage\QueueMessage; interface EventQueueInterface { /** * Write raw JSON payloads into the queue. * Called on the request path โ€” should return as fast as possible. * * @param list $payloads */ public function enqueue(array $payloads): void; /** * Pop up to $limit payloads as a list of QueueMessage. * Return an empty array when the queue is empty. * * @return list */ public function dequeue(int $limit): array; /** * Confirm successful delivery of the given messages. * * @param list $messages */ public function ack(array $messages): void; /** * Delivery failed โ€” return the messages to the queue for retry. * * @param list $messages */ public function nack(array $messages): void; } ``` **Implementation notes:** - `enqueue()` runs inside PHP-FPM request handling โ€” avoid expensive I/O operations (network round-trips, synchronous disk flushes, etc.) - `dequeue()` returns a `list`, where each `QueueMessage` contains an opaque `receipt` (implementation-defined acknowledgment token) and a `payload` (raw JSON string) - Claimed messages should have an expiration mechanism (e.g. TTL in Redis, scheduled cleanup in a database) so they are automatically recovered if a worker crashes ### Wiring Custom Adapters Pass your implementations via the `Config` and `ABConfig` constructors: ```php use SensorsWave\Client\Client; use SensorsWave\Config\Config; use SensorsWave\Config\ABConfig; $client = Client::create( 'https://your-endpoint.com', 'your-source-token', new Config( eventQueue: new YourEventQueue(/* ... */), // Custom event queue ab: new ABConfig( projectSecret: 'your-project-secret', abSpecStore: new YourABSpecStore(/* ... */), // Custom snapshot store ), ), ); ``` > [!WARNING] > > The request-path client and its corresponding worker must share the same storage backend: > - `ABSpecStoreInterface` โ€” written by `sensorswave-sync`, read by the request-path client > - `EventQueueInterface` โ€” written by the request-path client, read by `sensorswave-send` --- ## Predefined Properties The SDK provides predefined property constants for event tracking and user properties: ```php // Device and system properties '$app_version' // App version '$browser' // Browser name '$browser_version' // Browser version '$model' // Device model '$ip' // IP address '$os' // Operating system: ios/android/harmony '$os_version' // OS version // Geolocation properties '$country' // Country '$province' // Province/State '$city' // City ``` Using in events: ```php $client->trackEvent($user, 'Purchase', [ '$app_version' => '2.1.0', '$country' => 'US', 'product_id' => 'SKU-001', ]); ``` Using in A/B testing: ```php $user = $user->withAbUserProperty('$app_version', '2.1.0'); $user = $user->withAbUserProperty('$country', 'US'); ``` --- ## Complete API Method Reference ### Lifecycle Management | Method | Signature | Description | |--------|-----------|-------------| | **create** | `Client::create(string $endpoint, string $sourceToken, ?Config $config = null): Client` | Creates a client instance | | **close** | `close(): void` | Flushes in-memory events into the local queue and closes the client | | **flush** | `flush(): void` | Flushes the current buffered batch into the local queue without closing the client | ### User Identity | Method | Signature | Parameters | Return | Description | |--------|-----------|------------|--------|-------------| | **identify** | `identify(User $user): void` | `$user`: User with `anonId` and `loginId` | `void` | Creates a `$Identify` event linking anonymous and authenticated identities | ### Event Tracking | Method | Signature | Parameters | Return | Description | |--------|-----------|------------|--------|-------------| | **trackEvent** | `trackEvent(User $user, string $eventName, array\|Properties $properties = []): void` | `$user`: User identity; `$eventName`: Event name; `$properties`: Event properties | `void` | Primary method for tracking user actions with custom properties | | **track** | `track(Event $event): void` | `$event`: Fully populated event structure | `void` | Low-level API for advanced scenarios. Use `trackEvent` for normal usage | ### User Property Operations | Method | Signature | Description | Use Case | |--------|-----------|-------------|----------| | **profileSet** | `profileSet(User $user, array\|Properties $properties): void` | Sets or overwrites user properties | Update user name, email, settings | | **profileSetOnce** | `profileSetOnce(User $user, array\|Properties $properties): void` | Sets properties only if they don't exist | Record registration date, first source | | **profileIncrement** | `profileIncrement(User $user, array\|Properties $properties): void` | Increments numeric user properties | Login count, points, score | | **profileAppend** | `profileAppend(User $user, array\|ListProperties $properties): void` | Appends to list user properties (allows duplicates) | Add purchase history, activity log | | **profileUnion** | `profileUnion(User $user, array\|ListProperties $properties): void` | Adds unique values to list user properties | Add interests, tags, categories | | **profileUnset** | `profileUnset(User $user, string ...$propertyKeys): void` | Removes specified user properties | Clear temporary or deprecated fields | | **profileDelete** | `profileDelete(User $user): void` | Deletes all user properties (irreversible) | GDPR data deletion requests | ### A/B Testing | Method | Signature | Parameters | Return | Description | |--------|-----------|------------|--------|-------------| | **checkFeatureGate** | `checkFeatureGate(User $user, string $key): bool` | `$user`: User, `$key`: Gate key | `bool` | Evaluates a Feature Gate. Returns `false` if key not found or wrong type | | **getFeatureConfig** | `getFeatureConfig(User $user, string $key): ABResult` | `$user`: User, `$key`: Config key | `ABResult` | Evaluates a Feature Config. Returns empty result if key not found | | **getExperiment** | `getExperiment(User $user, string $key): ABResult` | `$user`: User, `$key`: Experiment key | `ABResult` | Evaluates an experiment. Returns empty result if key not found | | **evaluateAll** | `evaluateAll(User $user): array` | `$user`: User | `array` | Evaluates all loaded specs and records impression events | | **getABSpecs** | `getABSpecs(): string` | None | `string` | Exports current A/B metadata snapshot as JSON for caching and fast startup | --- ## Best Practices ### 1. Production Deployment Checklist Complete the following configuration before going live: ```php use SensorsWave\Client\Client; use SensorsWave\Config\Config; use SensorsWave\Config\ABConfig; $client = Client::create( 'https://your-endpoint.com', 'your-source-token', new Config( // โœ… Implement EventQueueInterface with your storage backend eventQueue: new YourEventQueue(/* ... */), // โœ… Register a failure callback for troubleshooting onTrackFailHandler: function (array $payloads, \Throwable $e) { error_log("Event enqueue failed: {$e->getMessage()}, lost " . count($payloads) . " events"); }, // โœ… Integrate your logging framework logger: new YourLoggerAdapter(/* ... */), ab: new ABConfig( projectSecret: 'your-project-secret', // โœ… Implement ABSpecStoreInterface with your storage backend abSpecStore: new YourABSpecStore(/* ... */), ), ), ); ``` ### 2. Resource Management Close the client at the end of each request to release resources: ```php $client = Client::create(...); try { // Business logic... $client->trackEvent($user, 'PageView', ['page' => '/home']); } finally { $client->close(); // Ensure client is properly closed } ``` ### 3. Error Handling In production, handle SDK errors gracefully without affecting the main business flow: ```php function trackUserEvent(Client $client, User $user, string $eventName, array $props): void { try { $client->trackEvent($user, $eventName, $props); } catch (\Exception $e) { // Log the error without affecting the main business flow error_log("Failed to track event: {$e->getMessage()}"); } } ``` ### 4. Event Naming Convention Use **PascalCase** for event names: ```php // โœ… Recommended: PascalCase $client->trackEvent($user, 'PageView', $props); $client->trackEvent($user, 'Purchase', $props); $client->trackEvent($user, 'AddToCart', $props); $client->trackEvent($user, 'UserRegistered', $props); // โŒ Avoid: snake_case (unless required by historical data) $client->trackEvent($user, 'page_view', $props); $client->trackEvent($user, 'purchase', $props); ``` **Naming guidelines:** - **Recommended**: Use PascalCase (e.g. `PageView`, `AddToCart`, `UserRegistered`) - **Legacy compatibility**: If existing data uses snake_case, you may continue with that style, but **stay consistent** - **Key point**: Whichever style you choose, **keep event naming consistent within the same project** ### 5. Property Naming Convention Use consistent `snake_case` naming for properties: ```php // Recommended [ 'product_id' => 'SKU-001', 'order_amount' => 99.99, 'user_tier' => 'premium', ] // Avoid [ 'ProductID' => 'SKU-001', // Avoid uppercase 'orderAmount' => 99.99, // Avoid camelCase ] ``` ### 6. Batch Operations For frequent user property updates, use incremental updates instead of multiple set calls: ```php $user = new User(loginId: $userId); // Not recommended: multiple calls $client->profileSet($user, ['login_count' => 1]); $client->profileSet($user, ['points' => 100]); // Recommended: single call $client->profileIncrement($user, [ 'login_count' => 1, 'points' => 100, ]); ``` --- ## FAQ ### Why are my events not being delivered? Check the following: 1. **Are worker processes running?** Confirm that the `sensorswave-send` worker is started and running properly 2. **Are the endpoint and token correct?** Verify the `endpoint` and `sourceToken` passed to the client 3. **Is the event queue functional?** Check read/write permissions for the default queue directory (`sys_get_temp_dir()`) 4. **Failure callback**: Check if `onTrackFailHandler` is configured and review any queue write failure logs 5. **Error logs**: Review the worker process log output ### Do worker processes need to run continuously? Yes. `sensorswave-send` delivers queued events to the server โ€” if it's not running, events will accumulate locally. `sensorswave-sync` synchronizes A/B metadata โ€” if it's not running, A/B evaluations will use stale snapshot data. Use Supervisor or systemd to manage worker processes and ensure automatic restarts on failure. ### Why is the PHP SDK architecture different from other language SDKs? The PHP/FPM process model means each request is an independent, short-lived process, making it unsuitable for remote HTTP calls on the request path. The SDK separates network I/O into independent worker processes, ensuring zero remote calls on the web request path and preserving response performance. ### Inconsistent A/B test evaluation results? Ensure: 1. **Consistent user properties**: Use the same A/B targeting properties for each evaluation 2. **Updated snapshots**: Confirm the `sensorswave-sync` worker is running and snapshot data is current 3. **Consistent adapters**: Ensure all web servers and worker processes use the same adapter configuration and share the same snapshot data ### How do I use the SDK in a distributed environment? When your application is deployed across multiple web servers, implement your own `EventQueueInterface` and `ABSpecStoreInterface` backed by a shared storage backend to ensure all servers and worker processes share the same A/B snapshots and event queue. See [Custom Adapters](#custom-adapters) for details. --- ## Related Documentation - [Tracking Strategy](../tracking-strategy.mdx): Understanding the tradeoffs between server-side and client-side tracking - [User Identification](../user-identification.mdx): Best practices for user identity management - [Data Model](../data-model.mdx): Understanding the Sensors Wave data structure - [Events and Properties](../events-and-properties.mdx): Event design guidelines and best practices --- **Last updated**: April 16, 2026 **SDK repository**: [github.com/sensorswave/sdk-php](https://github.com/sensorswave/sdk-php) **License**: See LICENSE file