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 for 99.9% uptime.

Installation

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.

  • TypingstartTyping, stopTypinguser_typing, user_stopped_typing
  • ReactionsaddReaction, removeReaction, getReactionsreaction_added, reaction_removed
  • ThreadsthreadReply, getThread, subscribeThread, followThread, markThreadReadthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmarkRead, markAllRead, getUnreadCountsuser_read, unread_count_updated, all_marked_read
  • MessageseditMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessages, searchMessagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statussetStatus, setCustomStatus, setDND, getUserPresenceuser_status_changed, custom_status_updated, dnd_status_changed
  • ChannelscreateChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannelchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMscreateDM, sendDM, getDMConversationsdm_created, dm_received
  • NotificationssubscribeNotifications, getNotifications, markNotificationRead, clearNotificationsnotification, 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 broadcastschallenge_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, 01

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

OddSockets PHP SDK delivers superior performance with broad compatibility:

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Messages/sec

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();