OddSockets PHP SDK

Official PHP SDK for OddSockets real-time messaging platform

PHP 8.1+ Composer ReactPHP High Performance JS Compatible

Overview & Features

The OddSockets PHP SDK provides a powerful, event-driven interface for real-time messaging in PHP applications, built on ReactPHP for high-performance async operations.

Modern PHP

Built for PHP 8.1+ with strict typing, union types, and modern language features.

Event-Driven

ReactPHP-based async architecture with EventEmitter pattern and Promise support.

JS Compatible

Identical API to JavaScript SDK for consistent cross-platform development.

High Performance

Optimized WebSocket connections with automatic reconnection and load balancing.

Cost Effective

No per-message pricing, industry-standard 32KB message limits, transparent pricing.

Automatic Failover

Built-in redundancy and intelligent error handling: the cluster reroutes you if a node goes away and the SDK resubscribes automatically.

Installation

Install from source for now. The Packagist package is not published yet, so the commands in the tabs below will not resolve. Use this instead — it pulls the same code the tabs will install once the package is live:
bash
composer config repositories.oddsockets vcs https://github.com/jyswee/oddsockets-php-sdk
composer require jyswee/oddsockets-php-sdk:dev-main
bash
composer require jyswee/oddsockets-php-sdk
json
{
  "require": {
    "php": "^8.1",
    "ext-json": "*",
    "ext-curl": "*",
    "guzzlehttp/guzzle": "^7.8",
    "ratchet/pawl": "^0.4",
    "react/promise": "^3.0",
    "evenement/evenement": "^3.0"
  }
}

Quick Start

Basic Usage

php
<?php

require_once 'vendor/autoload.php';

use OddSockets\OddSockets;
use React\EventLoop\Loop;

// Create client
$client = OddSockets::create('ak_live_1234567890abcdef');

// Get channel
$channel = $client->channel('my-channel');

// Subscribe to messages
$channel->subscribe(function ($message) {
    echo "Received: " . json_encode($message) . "\n";
});

// Publish a message
$channel->publish('Hello, World!');

// Start the event loop
Loop::get()->run();

Advanced Configuration

php
<?php

use OddSockets\Config\OddSocketsConfig;

$config = OddSocketsConfig::builder('ak_live_1234567890abcdef')
    ->userId('user123')
    ->autoConnect(false)
    ->reconnectAttempts(3)
    ->timeout(15)
    ->build();

$client = OddSockets::create($config);

$client->on('connected', function () {
    echo "Connected to OddSockets!\n";
});

$client->connect();

Promise-Based Operations

php
<?php

$channel = $client->channel('async-channel');

$channel->subscribe($callback)
    ->then(function () {
        echo "Successfully subscribed\n";
        return $channel->publish(['message' => 'Hello Async!']);
    })
    ->then(function ($result) {
        echo "Message published: " . json_encode($result) . "\n";
    })
    ->otherwise(function ($error) {
        echo "Error: " . $error->getMessage() . "\n";
    });

Configuration

Client Options

php
$client = OddSockets::create([
    'apiKey' => 'your-api-key',           // Required: Your OddSockets API key
    'userId' => 'user-id',                // Optional: User identifier
    'autoConnect' => true,                // Optional: Auto-connect on creation
    'reconnectAttempts' => 5,             // Optional: Max reconnection attempts
    'heartbeatInterval' => 30000          // Optional: Heartbeat interval (ms)
]);

Channel Options

php
$channel->subscribe($callback, [
    'enablePresence' => true,             // Enable presence tracking
    'retainHistory' => true,              // Retain message history
    'maxHistory' => 100                   // Maximum history size
]);

$channel->publish($message, [
    'ttl' => 3600,                        // Time to live (seconds)
    'metadata' => ['priority' => 'high'], // Additional metadata
    'storeInHistory' => true              // Store in message history
]);

Examples

Explore comprehensive examples demonstrating the OddSockets PHP SDK in action:

Enhanced Features

Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface: typing indicators, reactions, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives on the public $client->enhanced property. Send actions with $client->enhanced->* (camelCase) and receive the paired broadcasts with $client->on('<event>', $handler).

Typing & Reactions

php
<?php

use OddSockets\OddSockets;
use React\EventLoop\Loop;

$client = OddSockets::create(['apiKey' => 'ak_live_1234567890abcdef', 'userId' => 'alice']);

$client->connect()->then(function () use ($client) {
    $channel = $client->channel('room-42');
    $channel->subscribe(function ($msg) {}, ['enablePresence' => true]);

    // Receive-path: broadcasts from other users on the channel
    $client->on('user_typing', fn($e) => print("{$e['userId']} is typing\n"));
    $client->on('reaction_added', fn($e) => print("{$e['userId']} reacted {$e['emoji']}\n"));

    // Send-path: enhanced actions over the live socket
    $client->enhanced->startTyping('alice', 'room-42');
    $client->enhanced->addReaction('msg-1', 'room-42', ':thumbsup:', 'alice', 'Alice');
});

Loop::get()->run();

Threads

php
$client->on('thread_reply', fn($e) => print("New reply\n"));

$client->enhanced->threadReply('room-42', 'msg-1', 'Replying in the thread', 'alice', 'Alice');

// Query methods return ReactPHP promises
$client->enhanced->getThread('thread-1')->then(fn($thread) => var_dump($thread));

Enhanced surface

Each area exposes methods on $client->enhanced; the worker broadcasts the paired events which you handle with $client->on(...). Query methods (get*, search*) return promises that resolve with the worker response.

  • Typing — startTyping, stopTyping → user_typing, user_stopped_typing
  • Reactions — addReaction, removeReaction, getReactions → reaction_added, reaction_removed
  • Threads — threadReply, getThread, subscribeThread, followThread, markThreadRead → thread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receipts — markRead, markAllRead, getUnreadCounts → user_read, unread_count_updated, all_marked_read
  • Messages — editMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessages, searchMessages → message_edited, message_deleted, message_pinned, message_unpinned
  • Presence & status — setStatus, setCustomStatus, setDND, getUserPresence → user_status_changed, custom_status_updated, dnd_status_changed
  • Channels — createChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannel → channel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMs — createDM, sendDM, getDMConversations → dm_created, dm_received
  • Notifications — subscribeNotifications, getNotifications, markNotificationRead, clearNotifications → notification, notification_read, notifications_cleared

For any worker event not wrapped above, subscribe with the raw $client->on('<event>', $handler) API — all enhanced broadcasts are forwarded onto the client surface.

Challenges & Leaderboards

Challenges, leaderboards and achievements build on the same live socket. The send side lives on the enhanced surface ($client->enhanced->*); request/query methods return a ReactPHP PromiseInterface that resolves with the worker's reply (->then(...)), while progress and achievement calls are fire-and-forget. Inbound broadcasts arrive on the client event surface — subscribe with the client's normal $client->on('<event>', $handler).

php
$client->enhanced->createChallenge(['challengeId' => 'daily-sprint', 'metric' => 'points', 'ranked' => true]);

$client->enhanced->reportProgress(['challengeId' => 'daily-sprint', 'value' => 120]);

$client->enhanced->getStandings(['challengeId' => 'daily-sprint', 'limit' => 10])
    ->then(fn($board) => var_dump($board['standings'], $board['yourRank']));

$client->enhanced->completeChallenge(['challengeId' => 'daily-sprint', 'outcome' => 'completed']);

Methods

Request/query methods return a promise that resolves with the worker ack; progress and achievement calls are fire-and-forget (no ack).

  • createChallenge — create a challenge/leaderboard → ack challenge_create_success
  • reportProgress — fire-and-forget metric progress (no ack)
  • completeChallenge — finalize with an outcome → ack challenge_complete_success
  • unlockAchievement — fire-and-forget; pass percentComplete (0–100) (no ack)
  • getStandings — request top-N + caller rank → ack challenge_standings_success
  • getAchievements — query achievement state → ack achievement_state
  • sendChallengeInvite — directed invite to another user → ack challenge_invite_success
  • replyChallengeInvite — accept/decline an invite → ack challenge_reply_success
  • cancelChallengeInvite — cancel a sent invite → ack challenge_invite_cancel_success
  • getChallengeInvites — list pending invites → ack challenge_invites

Completion outcomes

The outcome passed to completeChallenge is one of:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — timed out

Progressive achievements

unlockAchievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete. A value below 100 broadcasts achievement_progress (status in_progress); a value of 100 or omitted broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.

Inbound events

Subscribe with the client's normal $client->on('<event>', $handler).

  • Room broadcasts — challenge_progress, leaderboard_rank_change, challenge_complete, achievement_unlock, achievement_progress
  • Directed (per-user) — challenge_invited, challenge_reply_received, challenge_invite_cancelled

Usage Analytics

Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. getUsageStats() resolves the same four tiles the developer dashboard renders.

$stats = $client->getUsageStats()->wait(); // returns a UsageStats

// Each accessor is int|float|null — null means "no data", so render an em-dash.
$tile = fn ($value) => $value === null ? "\u{2014}" : $value;

echo "MAU:      " . $tile($stats->getMau()) . PHP_EOL;
echo "DAU:      " . $tile($stats->getDau()) . PHP_EOL;
echo "Messages: " . $tile($stats->getTotalMessages()) . PHP_EOL;
echo "Errors:   " . $tile($stats->getErrorRate()) . PHP_EOL;

The four tiles

  • getMau() — monthly active users for your owner scope
  • getDau() — daily active users
  • getTotalMessages() — total messages published
  • getErrorRate() — publish error rate, 0–1

Honesty rule — null, never a fake zero

Each tile is a number or null. A null means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns null verbatim and never coerces it to 0. Render an em-dash (—) for a null tile so you never show a fabricated zero.

Requires an API key

getUsageStats() reads your owner-scoped analytics, so it needs an apiKey. Keyless / token-only clients have no owner scope to query and the promise will reject with getUsageStats requires an apiKey.

Performance & Compatibility

Platform limits and plan ceilings, plus the versions and platforms this SDK supports:

32KB
Max message size
100M
Messages/mo (Scale)
5,000
Peak connections (Scale)
99.999%
Uptime SLA (Enterprise)

PHP Support

  • PHP 8.1+ (Latest)
  • ReactPHP Ecosystem
  • Composer Package
  • PSR-4 Autoloading

Framework Support

  • Laravel Integration
  • Symfony Compatible
  • Standalone Usage
  • ReactPHP Apps

Framework Integrations

The OddSockets PHP SDK works seamlessly with popular PHP frameworks. Here are examples showing how to integrate:

Laravel

php
<?php

namespace App\Services;

use OddSockets\OddSockets;
use React\EventLoop\Loop;

class OddSocketsService
{
    private $client;
    
    public function __construct()
    {
        $this->client = OddSockets::create(config('oddsockets.api_key'));
        
        $this->client->on('connected', function () {
            \Log::info('OddSockets connected');
        });
    }
    
    public function publishToChannel(string $channel, $message)
    {
        $channel = $this->client->channel($channel);
        
        return $channel->publish($message)
            ->then(function ($result) {
                \Log::info('Message published', $result);
                return $result;
            })
            ->otherwise(function ($error) {
                \Log::error('Publish failed', ['error' => $error->getMessage()]);
                throw $error;
            });
    }
    
    public function startEventLoop()
    {
        Loop::get()->run();
    }
}

Symfony

php
<?php

namespace App\Service;

use OddSockets\OddSockets;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class OddSocketsService
{
    private $client;
    private LoggerInterface $logger;
    
    public function __construct(
        #[Autowire('%env(ODDSOCKETS_API_KEY)%')] string $apiKey,
        LoggerInterface $logger
    ) {
        $this->logger = $logger;
        $this->client = OddSockets::create($apiKey);
        
        $this->client->on('connected', function () {
            $this->logger->info('OddSockets connected');
        });
        
        $this->client->on('error', function ($error) {
            $this->logger->error('OddSockets error', ['error' => $error->getMessage()]);
        });
    }
    
    public function subscribe(string $channelName, callable $callback): void
    {
        $channel = $this->client->channel($channelName);
        
        $channel->subscribe($callback)
            ->then(function () use ($channelName) {
                $this->logger->info('Subscribed to channel', ['channel' => $channelName]);
            })
            ->otherwise(function ($error) use ($channelName) {
                $this->logger->error('Subscription failed', [
                    'channel' => $channelName,
                    'error' => $error->getMessage()
                ]);
            });
    }
}

ReactPHP Application

php
<?php

require_once 'vendor/autoload.php';

use OddSockets\OddSockets;
use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;

$loop = Loop::get();

// Create OddSockets client
$oddSockets = OddSockets::create('ak_live_1234567890abcdef', $loop);

// Create HTTP server
$server = new HttpServer($loop, function ($request) use ($oddSockets) {
    $channel = $oddSockets->channel('web-events');
    
    // Publish web request event
    $channel->publish([
        'type' => 'http_request',
        'method' => $request->getMethod(),
        'uri' => $request->getUri()->getPath(),
        'timestamp' => time()
    ]);
    
    return new Response(200, [], 'Hello from ReactPHP + OddSockets!');
});

// Subscribe to events
$eventChannel = $oddSockets->channel('system-events');
$eventChannel->subscribe(function ($message) {
    echo "System event: " . json_encode($message) . "\n";
});

// Start servers
$socket = new SocketServer('127.0.0.1:8080', [], $loop);
$server->listen($socket);

echo "Server running on http://127.0.0.1:8080\n";
echo "OddSockets integration active\n";

$loop->run();