jdare/clank-bundle
Clank Bundle integrates the Clank PHP Slack bot library with your framework, adding configuration and service wiring so you can register bots and handle Slack events without manual bootstrapping.
This is a Symfony 2.x bundle, not a Laravel package. Treat it as two things: (a) a turnkey WebSocket server for legacy Symfony 2.1–2.3 apps, and (b) a reference implementation of Ratchet + a topic/RPC dispatcher that you can port into a Laravel artisan command in an afternoon. Its actual value to a Laravel dev is the architecture — WAMP topic handlers as DI services, session-aware handshake, and a ZMQ "push from HTTP request → WS server" bridge — not the composer require.
Minimal steps in a Symfony 2 app:
composer require jdare/clank-bundle (expect PHP 5.3/5.4-era constraints and Symfony 2.x components — this will not resolve against a modern Laravel composer.json).new Clank\Bundle\ClankBundle() in AppKernel, add a clank: block to config.yml for host/port and session options (read DependencyInjection/Configuration.php first — the keys changed between releases and the README lags).@Topic("chat") with onSubscribe / onPublish / onClose, register it as a service tagged clank.topic.php app/console clank:server:start and connect with AutobahnJS.First use case: a presence/notification channel. Subscribe a connection to a per-user topic on handshake, broadcast on state change, remove on close. That exercises the session provider, the topic tag, and the broadcaster — the three things you'll port.
Where to look first, in this order: the clank.topic tag compiler pass (shows how topic names are resolved), the topic dispatcher (the WAMP glue), the Symfony session provider (the auth story), and the server command (the React loop you'll reimplement). Skip the bundle's DI bootstrapping entirely when porting.
One topic class per domain, registered as a service. The bundle scans tagged services for the @Topic annotation, so constructor injection works normally:
/**
* @Topic("chat")
*/
class ChatTopic
{
public function onSubscribe(ConnectionInterface $conn, $topic)
{
$topic->broadcast(['msg' => "{$conn->resourceId} joined"]);
}
public function onPublish(ConnectionInterface $conn, $topic, $msg)
{
$topic->broadcast(['msg' => $msg, 'from' => $conn->resourceId]);
}
public function onClose(ConnectionInterface $conn, $topic)
{
$topic->broadcast(['msg' => "{$conn->resourceId} left"]);
}
}
services:
acme.topic.chat:
class: Acme\ChatBundle\Topic\ChatTopic
arguments: ['@doctrine.orm.entity_manager']
tags:
- { name: clank.topic }
Authorize inside onSubscribe, not at the router. The session provider only proves identity at handshake time; per-topic authorization is your job. $conn->Session->get('user_id') is populated from the Symfony session cookie during the upgrade request. If you're porting to Laravel, that's the hook where you'd swap in a JWT-in-query-string or a signed-cookie SessionProvider.
Push from HTTP to the socket server. The bundle's push service lets a controller notify connected clients without the socket server making an outbound request — it publishes over ZMQ to the React loop, which then broadcasts to the topic. This is the pattern worth stealing: in Laravel, put a Redis PUBLISH in a controller/event listener and a React\ZMQ or clue/redis-react subscriber inside the artisan WS loop. Keep the message envelope identical on both ends (topic name + payload) so the loop is a dumb relay.
RPC handlers (@Rpc classes with onCall) are useful for request/response over WS — polling, "fetch my unread count" — but they give you a second RPC surface with no auth model of its own. Prefer topics plus a thin REST call unless you specifically need sub-100ms round trips.
Operational wiring: run the command under Supervisor with autorestart=true; front it with nginx using proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; and proxy_read_timeout 3600s; terminate TLS at nginx rather than in React.
Porting to Laravel — the whole bundle collapses into ~80 lines:
protected $signature = 'ws:serve {--port=8080}';
public function handle()
{
$loop = \React\EventLoop\Loop::get();
$socket = new \React\Socket\SocketServer("0.0.0.0:{$this->option('port')}", [], $loop);
$server = new \Ratchet\Server\IoServer(
new \Ratchet\Http\HttpServer(
new \Ratchet\WebSocket\WsServer(
new \Ratchet\Wamp\WampServer(app(TopicDispatcher::class))
)
),
$socket,
$loop
);
$server->run();
}
TopicDispatcher implements WampServerInterface and resolves topic names against a ['chat' => ChatTopic::class] map from a config file — that map replaces the bundle's annotation scan and is easier to reason about anyway.
cboden/ratchet together and freeze that corner of the dependency tree.@Topic names are a string contract with the JS client. A typo yields a silent no-op subscribe, not an error. Log the topic name in onSubscribe while developing.doctrine/annotations and a writable cache. In prod, the annotation reader cache goes stale after deploys — restart the process, don't just clear the app cache. This is the most common "my new topic doesn't exist" cause.$em->clear() / DB::disconnect() in a timer, and always restart the server on deploy — there is no hot reload.skipSubprotocolCheck: true against this server, otherwise the connection is rejected for protocol mismatch. Clients written against other Ratchet examples will fail here first.error, and consider a Redis pub/sub fallback that survives server restarts.clank:server:start -v surfaces Ratchet exceptions that otherwise die on the connection; attach a PSR-3 logger to IoServer in your port for the same visibility. Most "connection drops immediately" reports are an uncaught exception in onSubscribe.How can I help you explore Laravel packages today?