Broadcasting¶
Note
Not part of core. Install it separately:
composer require kinetis/broadcasting
Real-time updates over the Pusher Channels wire protocol — Soketi,
Laravel Reverb, and Pusher’s own hosted
service all implement it identically, so one driver covers all three;
only the host, port, and TLS setting differ between them.
Kinetis\Broadcasting\Driver\PusherBroadcaster sends over
kinetis/revolt-http-client, so triggering an event suspends the
calling Fiber rather than blocking the worker while the broker accepts
it.
Nothing here runs a WebSocket server — Kinetis stays an HTTP framework.
Point BROADCAST_HOST/BROADCAST_PORT at Soketi, Reverb, or Pusher, and
this package handles the two things an HTTP process actually does in a
broadcasting setup: signing the trigger request that pushes an event,
and signing the subscription request a client makes before it can join
a private or presence channel.
Sending an event¶
Two shapes, both reaching Kinetis\Broadcasting\BroadcasterInterface
underneath — a controller or a #[Listener] method constructor-injects
Kinetis\Broadcasting\Broadcaster:
use Kinetis\Broadcasting\Broadcaster;
final readonly class TrackShipment
{
public function __construct(private Broadcaster $broadcaster) {}
public function markShipped(string $orderId): void
{
// ... update the order ...
$this->broadcaster->broadcast('private-orders.' . $orderId, 'order.shipped', [
'status' => 'shipped',
]);
}
}
Or describe an event once and broadcast it by value — implement
Kinetis\Broadcasting\ShouldBroadcast on the DTO your own code
already dispatches:
use Kinetis\Broadcasting\ShouldBroadcast;
final readonly class OrderUpdated implements ShouldBroadcast
{
public function __construct(
private string $orderId,
private string $status,
) {}
public function broadcastOn(): array
{
return ["private-orders.{$this->orderId}"];
}
public function broadcastAs(): string
{
return 'order.updated';
}
public function broadcastWith(): array
{
return ['status' => $this->status];
}
}
$broadcaster->event(new OrderUpdated($orderId, 'shipped'));
Broadcaster::event() calls
Kinetis\Broadcasting\BroadcasterInterface’s broadcast() once
per channel broadcastOn() names, with broadcastAs()’s event name and
broadcastWith()’s payload.
ShouldBroadcast is deliberately not wired into
Kinetis\Events\EventDispatcher automatically — unlike
Kinetis\Events\ShouldQueue (checked per listener, inside a dispatch
loop already built for it), whether an event broadcasts is a per-event
concern with no natural hook in that loop. Call Broadcaster::event()
explicitly, typically from inside the #[Listener] method that would
otherwise dispatch a queued job for the same event.
Configuring¶
BROADCAST_DRIVER=pusher
BROADCAST_APP_ID=your-app-id
BROADCAST_KEY=your-key
BROADCAST_SECRET=your-secret
BROADCAST_HOST=soketi.example.com
BROADCAST_PORT=6001
BROADCAST_TLS=false
BROADCAST_DRIVER defaults to null — Kinetis\Broadcasting\NullBroadcaster,
a silent no-op, the same “sensible default nobody has to opt into”
pattern LoggerInterface → NullLogger already establishes. Setting it
to pusher additionally requires BROADCAST_APP_ID/BROADCAST_KEY/
BROADCAST_SECRET — validated and the driver built at worker boot, not
lazily, so a misconfiguration fails before the first request rather than
on whichever one happens to broadcast first. Every key is
Config::scopedKey()-scoped for named connections:
BROADCAST_KEY + notifications → BROADCAST_NOTIFICATIONS_KEY.
Pointed at a real Pusher account, drop BROADCAST_HOST/BROADCAST_PORT/
BROADCAST_TLS and use the account’s own cluster — api-{cluster}.pusher.com,
port 443, TLS on (the defaults).
Verified¶
PusherBroadcaster’s signing algorithm is checked against
pusher/pusher-php-server’s own real source, not reconstructed from
documentation, and every private/presence channel authorization and
trigger request in this package’s own test suite is checked against an
independently computed HMAC-SHA256 signature. Beyond that, the full
chain — attribute discovery, CurrentUserInterface resolution,
authorization, and both channel-auth response shapes — has been run
end to end against a real Soketi broker and a real WebSocket client: a
public-channel broadcast delivered to a subscriber, a private-channel
subscription signed by this package’s own driver accepted by Soketi
with the triggered event delivered to it, and a presence-channel
subscription signed by the real BroadcastAuthController accepted with
the correct channel_data reflected back.