Cable: Real-Time Communication for Lightpack

Cable is Lightpack's elegant solution for real-time communication between your server and clients. With a Socket.io-like API familiarity and a focus on simplicity and efficiency, Cable provides powerful real-time features without external dependencies.

You can build live dashboards, notifications, presence channels, chat messages and more with minimal friction. It uses an efficient HTTP Polling mechanism to provide real-time communication capabilities:

Most app don't need sub-second updates and hence not worth the infrastructure complexity of Websocket implementation. Real-time doesn't always mean WebSockets!

Cable Event Flow (Lifecycle)

→ PHP emits event 
→ Driver stores event 
→ Browser polls 
→ New events delivered 
→ JS handler runs

Core Concepts


Quick Start

Lightpack already ships with few route definitions for making it easy to work with message polling or presence channels. You can look for routes/cable.php file for more details. Following sections gives you a quick overview to get started with realtime features.

1. Configure Cable

Please run following command to create config/cable.php configuration file.

php console create:config --support=cable

Choose a driver:

2. Run Migration

If you use database as backend for cable features, you need to migrate schema.

Create schema migration file:

php console create:migration --support=cable

Run migration:

php console migrate:up

3. Define Route

route()->post('/realtime/send-message', RealtimeController::class, 'triggerMessage');

This route will forward the POST /realtime/send-message request to RealtimeController's triggerMessage() method.

4. Emit Events

In your controller's method, emit an event message:new to the notifications channel with required payload:

class RealtimeController
{
    public function triggerMessage(Cable $cable)
    {
        $cable->to('notifications')->emit('message:new', [
            'text' => request()->input('message'),
        ]);

        return response()->json(['success' => true]);
    }
}

5. Receive Events

In your frontend, include the cable.js script file:

<?= asset()->load('js/cable.js') ?>

Initialize cable client and subscribe to notifications channel events:

// Connect to Cable
const socket = cable.connect();

// Subscribe to `message:new` event on notifications channel 
socket
    .subscribe('notifications')
    .on('message:new', function(data) {
        console.log('New message:', data.text);
    });

Channel-Based Communication

Channels allow you to organize your real-time communication. Your application emits events on a channel, whereas your frontend client subscribes to events on a channel.

Emitting Events

// Send to a specific user
$cable->to("user.{$userId}")->emit('private-message', [
    'text' => 'This is a private message'
]);
// Send to a group
$cable->to('admin-notifications')->emit('system-alert', [
    'level' => 'warning',
    'message' => 'Disk space is low'
]);
// Broadcast to everyone
$cable->to('broadcasts.all')->emit('announcement', [
    'message' => 'Site maintenance in 10 minutes'
]);

Subscribing to Events

Your frontend client can subscribe to emitted events without requiring to manually poll for new messages:

const subscription = socket.subscribe('another-channel');

subscription.on('event-one', function(data) {
    console.log('Event one:', data);
});

subscription.on('event-two', function(data) {
    console.log('Event two:', data);
});

Custom Configurations

You can customize following option when initializing a cable connection.

const socket = cable.connect(
    endpoint: '/cable/poll',
    pollInterval: 3000,
    reconnectInterval: 5000,
    maxReconnectAttempts: 5
)

DOM Updates

Cable can directly update DOM elements without writing custom JavaScript:

// Update a specific element by selector
$cable->to('dashboard')->update(
    '#user-count', 
    "<strong>{$userCount}</strong> users online"
);
// Update multiple elements with the same selector
$cable->to('dashboard')->update(
    '.status-indicator', 
    '<span class="online"></span>'
);

On the client-side, this is handled automatically - no additional code needed!

Cable API Reference

to(string $channel): self

Targets a specific channel for subsequent events.

// Targeting a chat channel
$chat = $cable->to('chat:42');

emit(string $event, array $payload = [])

Sends an event (with payload) to the current channel.

// Emitting a new chat message
$cable->to('chat:42')->emit('message:new', [
    'user' => 'alice',
    'text' => 'Hello, world!'
]);

update(string $selector, string $html)

Emits a dom-update event, allowing you to update parts of the UI in real time.

// Live update a status element on all dashboards
$cable->to('dashboard')->update('#status', '<span>Online</span>');

getMessages(?string $channel = null, ?int $lastId = null): array

Retrieves new messages for a channel since a given message ID. Used by polling clients (normally not called directly).

// Fetch new messages for chat:42 since message ID 100
$messages = $cable->getMessages('chat:42', 100);
foreach ($messages as $msg) {
    echo $msg->event . ': ' . json_encode($msg->payload);
}

cleanup(int $olderThanSeconds = 86400)

Deletes old messages from the backend (maintenance/housekeeping).

// Remove messages older than 1 hour
$cable->cleanup(3600);

You can run a schedule to routinely cleanup old messages.

Presence Channels

Presence channels allow you to easily track which users are online in real-time.

Note: You must define the following meta tag inside tag. This is automatically used by the cable.js to pass CSRF token in request header for presence endpoints.

<meta name="csrf-token" content="<?= csrf_token() ?>">

Initialize the channel presence

// Define channel name
const channel = 'presence-room';

// Initialize Cable
const socket = cable.connect();

// Initialize presence channel
const presence = socket.presence(channel, userId);

Now you can utilize the presence channel capabilities as documented below:

Join the presence channel

presence.join()
    .then(() => {
        console.log('Joined presence channel');

        // Start heartbeat to maintain presence
        presence.startHeartbeat();
    });

Leave presence channel

presence.stopHeartbeat();
presence.leave()
    .then(() => {
        console.log('Left presence channel');
    });

Get all users in channel

presence.getUsers()
    .then(data => {
        console.log('Users in channel:', data.users);
    });

Subscribe to presence channel events

socket.subscribe(channel)

    // Handle presence updates
    .on('presence:update', function(data) {
        console.log('Presence update:', data);
        // updateOnlineUsers(data.users);
    })

    // Handle join events
    .on('presence:join', function(data) {
        console.log('Users joined:', data.users);
        data.users.forEach(id => {
            // addNotification(`User ${id} joined the chat`);
        });
    })

    // Handle leave events
    .on('presence:leave', function(data) {
        console.log('Users left:', data.users);
        data.users.forEach(id => {
            // addNotification(`User ${id} left the chat`);
        });
    })

    // Handle message events
    .on('message', function(data) {
        console.log('Received message event with data:', data);
        // addMessage(data.userId, data.text);
    });

Presence Channel Configuration

// Join a presence channel with custom endpoint
presence.join('/custom/join/endpoint');

// Leave a presence channel with custom endpoint
presence.leave('/custom/leave/endpoint);

// Start heartbeat with custom interval and endpoint
presence.startHeartbeat(30000, '/custom/heartbeat/endpoint');

// Get users with custom endpoint
presence.getUsers('/custom/users/endpoint');

Message Batching

You can batch multiple messages together to promote better performance:

  1. Fewer database writes
  2. Better transaction handling
  3. Reduced server load
  4. More efficient client updates

Server-Side Batching

Without Batching (BAD)

public function notifyUsers(Cable $cable)
{
    // Sending 100 notifications = 100 database writes
    foreach($users as $user) {

        $cable->to('notifications')->emit('message:new', [
            'user' => $user->id,
            'message' => 'Hello'
        ]);

    }
}

With Batching (GOOD)

public function notifyUsers(MessageBatcher $batcher)
{
    $batcher->channel('notifications')->batchSize(100);

    // Sending 100 notifications = 1 database writes
    foreach($users as $user) {

        $batcher->add('message:new', [
            'user' => $user->id,
            'message' => 'Hello'
        ]);

    }
}

The default batch size is 100 which you can override. You never need to manually flush the batch of messages. it is automatically done by the MessageBatcher instance.

Client-Side Batching

const socket = cable.connect();
const channel = 'user-tracking';

// Configure client-side batching
socket.setBatchOptions({
    batchSize: 5,           // Max events per batch
    batchInterval: 2000,     // Flush interval in ms
    batchEndpoint: '<?= url()->route('api.batch-events') ?>',
    csrfToken: '<?= csrfToken() ?>',
});

// Track events (added to batch)
socket.emitBatched(channel, 'event:button-clicked', { data: 'Subscribe Button' });
socket.emitBatched(channel, 'event:mouse-moved', { data: 'FAQ Section' });
socket.emitBatched(channel, 'event:button-hovered', { data: 'Buy Now Button' });
...
...
...
socket.emitBatched(channel, 'event:button-clicked', { data: 'Buy Now Button' });

Once the batch size exceeds or batch interval passes, the batch endpoint is automatically called with all the even data.

If required, you can manually flush the batch of events:

socket.flushOutgoingBatch();

Notification Sounds

An interesting feature of Lightpack cable.js client is that it can play sounds for subcribed events:

cable.playSound('/sounds/notify.mp3');

For example:

socket
    .subscribe('notifications')
    .on('message', function(data) {
        cable.playSound('/sounds/notify.mp3', 0.7);
    });