Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Centrifugo Bundle Laravel Package

kismia/centrifugo-bundle

Symfony bundle for integrating the Centrifugo real-time messaging server, providing services and configuration to publish events and enable websocket-based live updates in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

The honest framing first: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no auto-discovery, and no config/centrifugo.php shipping in the box. The Symfony bundle (Kismia\CentrifugoBundle\CentrifugoBundle) registers a DI service that wraps the Centrifugo HTTP API behind config keys (typically api_url, api_key, secret, timeout). For a Laravel team the value is as a reference implementation you port in ~30 minutes, or as a signal to use the underlying Centrifugo HTTP API directly. With 11 stars, zero dependents, and a 0.085 health score, do not make this a runtime dependency of a Laravel app — vendor it, mirror it, or write the ~80 lines yourself.

Where to look first when you clone it:

  1. DependencyInjection/Configuration.php — the canonical list of config keys and defaults. This is your config/centrifugo.php blueprint.
  2. DependencyInjection/CentrifugoExtension.php — how the service is assembled (URL normalization, HTTP client choice, header injection).
  3. Service/Centrifugo.php (or similar) — the actual API surface: publish, broadcast, presence, history, disconnect, maybe unsubscribe. That method list is your target interface.

Minimal Laravel port — the first thing you build:

// config/centrifugo.php
return [
    'url'       => env('CENTRIFUGO_API_URL', 'http://centrifugo:8000/api'),
    'api_key'   => env('CENTRIFUGO_API_KEY'),
    'secret'    => env('CENTRIFUGO_TOKEN_SECRET'),
    'timeout'   => env('CENTRIFUGO_TIMEOUT', 2),
];
// app/Services/CentrifugoClient.php
namespace App\Services;

use Illuminate\Support\Facades\Http;

class CentrifugoClient
{
    public function __construct(private array $config) {}

    public function publish(string $channel, array $data): array
    {
        return Http::withHeaders(['X-API-Key' => $this->config['api_key']])
            ->timeout($this->config['timeout'])
            ->post($this->config['url'], [
                'method' => 'publish',            // v3-style; see Gotchas for v5
                'params' => ['channel' => $channel, 'data' => $data],
            ])
            ->throw()
            ->json();
    }
}
// app/Providers/CentrifugoServiceProvider.php
public function register(): void
{
    $this->mergeConfigFrom(base_path('config/centrifugo.php'), 'centrifugo');
    $this->app->singleton(CentrifugoClient::class,
        fn ($app) => new CentrifugoClient($app['config']['centrifugo']));
}

First use case: publish to a channel after a domain event (chat message, order status, notification) and hand the browser a connection JWT. That pair of features is 90% of real Centrifugo usage; everything else (presence, history) is opt-in and needs server config you probably haven't enabled yet.

Implementation Patterns

Publish-after-commit is the default pattern. Never publish from inside a transaction handler — subscribers will receive an event for a row they can't yet read.

class PublishChatMessage
{
    public function handle(MessageSent $event): void
    {
        DB::afterCommit(fn () => app(PublishToCentrifugo::class)
            ->dispatch("chat:{$event->message->room_id}", [
                'id'     => $event->message->id,
                'author' => $event->message->user_id,
                'body'   => $event->message->body,
            ]));
    }
}

Queue the publish. Centrifugo HTTP is fast but it is still a network hop; do it in a ShouldQueue job so a slow Centrifugo (or a redeploy) doesn't add latency to your request path. Use retry(3) with backoff, and make payloads idempotent (id field, idempotency_key param on v5) so retries are safe.

Issue JWTs from Laravel, not from the frontend. Centrifugo's connection token is a JWT signed with token_hmac_secret_key (or RSA private key). sub is the user ID; info is JSON merged into presence; channels restricts subscriptions.

public function connectionToken(string $userId, array $info = []): string
{
    $now = time();
    return JWT::encode([
        'sub'  => $userId,
        'iat'  => $now,
        'exp'  => $now + 3600,
        'info' => $info,           // or b64info if you want it base64'd
    ], config('centrifugo.secret'), 'HS256');
}

For private channels ($ or # prefixed), also mint a subscription token containing the channel claim — clients pass it to subscribe(). Same secret, different token.

Wrap the client in your own interface. interface RealtimePublisher { public function publish(string $channel, array $data): void; }. Bind the Centrifugo implementation in the service provider. This makes testing trivial (bind an in-memory recorder) and makes a future swap to Pusher/Ably/Soketi a config change, not a refactor.

Laravel-native front door. Let Laravel events and broadcasting be the API your app calls; a single listener fans out to Centrifugo. It keeps channel naming, payload shape, and token logic in one place instead of scattered across controllers.

Presence and history need both sides. Calling presence($channel) returns nothing useful unless the channel's namespace enables presence: true and history_size/history_ttl in Centrifugo's own config. Treat these as server-config features, not client features.

Testing. Http::fake() in Laravel, Mockery for the Symfony original. Assert the outgoing body matches the Centrifugo API shape you're actually targeting (this is where version drift bites — see below). Add one integration test against a real Centrifugo container in CI; a docker run centrifugo/centrifugo with a throwaway config is cheap and catches header/endpoint mistakes that fakes never will.

Gotchas and Tips

  • Version mismatch is the #1 failure mode. Centrifugo v2/v3 expose a single POST /api with {"method": "...", "params": {...}}. Centrifugo v4/v5 expose per-method endpoints (POST /api/publish) with a flat body and X-API-Key. This bundle likely targets the older shape. Confirm which one your Centrifugo is running before copying method names — a 404 or a 200-with-error body will both look like "silent failure."
  • A 200 response is not success. Centrifugo returns HTTP 200 with {"error": {...}} for API-level failures (bad channel, disabled namespace, malformed data). ->throw() will not catch them. Check $response['error'] yourself or you'll ship a broken feature that "works" in logs.
  • Two secrets, not one. api_key (HTTP API auth) and token_secret (token_hmac_secret_key, JWT signing) are distinct, live in different places in Centrifugo config, and rotating one without the other silently locks out clients vs. backends. Most misconfiguration traces back to using one where the other is expected.
  • data must be JSON-serializable. PHP arrays with mixed keys become objects; empty arrays become [] not {} and can break clients expecting an object. Cast payloads explicitly.
  • Timeouts and retries. The Symfony bundle delegates to Guzzle; Laravel's Http client is GuzzleHttp\Client under the hood, so behavior is similar. Set a short timeout (2–3s) and a connect_timeout; Centrifugo can be slow on cold start. Never let a realtime publish block a user request.
  • Publishing before commit is the classic bug: subscriber receives an event, queries the API, gets a 404 for the row, and the frontend shows a phantom. DB::afterCommit or dispatchAfterCommit on the job.
  • Channel naming is a contract. Pick a scheme (chat:{roomId}, #user:{id}, private-org:{id}) and document it. Private prefixes ($, #) trigger subscription-token checks; public channels don't. Retrofit later is painful because clients already assume the naming.
  • Symfony auto-wiring does not apply. If you're actually working in Symfony, register the bundle in bundles.php and configure under the bundle's own root key (kismia_centrifugo:). If you're in Laravel, remember there is no package:discover — manual mergeConfigFrom and a singleton are all you get.
  • Low-maintenance vendor risk. 11 stars, 0 dependents, score 0.085 — assume no security patches and no Centrifugo v5 compatibility work. Pin to a commit if you fork it, keep the Symfony source in a docs/reference/ folder or a private namespace, and don't spread its types through your codebase. The value is the shape of the client, not the package itself.
  • Debugging order: curl -X POST $CENTRIFUGO_API_URL -H "X-API-Key: $KEY" -d '{"method":"publish","params":{"channel":"test","data":{"ping":1}}}' first → then Laravel's HTTP client with Http::dd() → then Centrifugo's server logs (debug: true in its config). Most failures are endpoint shape, API key, or channel namespace permissions, in that order.
  • Extension points worth keeping: a compiler pass/service decorator in Symfony maps to binding your own RealtimePublisher interface in Laravel. Also expose a Centrifugo::fake()-style test double early — once teams start calling it from model observers, a fake is the only way to keep the suite fast.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk