oleh-ozimok/php-centrifugo
A PHP client for Centrifugo, the scalable real-time messaging server. Publish messages to channels via the HTTP API, push live updates to websocket clients, and integrate real-time features into Laravel or any PHP application with a simple, framework-agnostic interface.
Install the package
composer require oleh-ozimok/php-centrifugo
There is no Laravel auto-discovery service provider — this is a framework-agnostic library. Confirm the actual namespace/class name by listing the package source, e.g. ls vendor/oleh-ozimok/php-centrifugo/src. Expect something like Ozimok\Centrifugo\Client constructed with an endpoint URL and API key.
Add config in config/services.php so credentials never live in code:
'centrifugo' => [
'url' => env('CENTRIFUGO_URL', 'http://127.0.0.1:8000'),
'api_key' => env('CENTRIFUGO_API_KEY'),
],
And in .env:
CENTRIFUGO_URL=http://127.0.0.1:8000
CENTRIFUGO_API_KEY=your-secret-key
Bind it as a singleton in App\Providers\AppServiceProvider::register() so you get connection reuse and a single place to swap/mock it:
$this->app->singleton(\Ozimok\Centrifugo\Client::class, function () {
return new \Ozimok\Centrifugo\Client(
config('services.centrifugo.url'),
config('services.centrifugo.api_key')
);
});
First use case — publish a change from a controller after a model is saved:
public function update(Request $request, Post $post, \Ozimok\Centrifugo\Client $centrifugo)
{
$post->update($request->validated());
$centrifugo->publish("post.{$post->id}", [
'event' => 'post.updated',
'body' => $post->toArray(),
]);
return response()->json($post);
}
If the publish call fails, the first thing to check is whether your Centrifugo server version matches the endpoint/auth scheme this 2018-era client expects (see Gotchas).
Always publish from the server, never the browser. The client is a backend HTTP wrapper around Centrifugo's publish API; the API key must never reach the frontend. Laravel is the trust boundary.
Wrap the library in your own thin service. This keeps the third-party class out of controllers and gives you a stable seam for testing:
namespace App\Services;
class Broadcaster
{
public function __construct(private \Ozimok\Centrifugo\Client $client) {}
public function toChannel(string $channel, array $payload): void
{
$this->client->publish($channel, $payload);
}
}
Publish after commit, not inside the transaction. Emitting a real-time event before the DB commits means a client can react to data that later rolls back:
DB::transaction(function () use ($post, $broadcaster) {
$post->update([...]);
});
$broadcaster->toChannel("post.{$post->id}", ['event' => 'post.updated', 'body' => $post]);
Or push it to a listener on PostUpdated and have that listener implement ShouldQueue/$afterCommit = true.
Prefer queue jobs for fan-out. Publishing in a queued job keeps request latency flat and gives you retries:
class BroadcastPostUpdate implements ShouldQueue
{
public function __construct(public int $postId) {}
public function handle(Broadcaster $broadcaster): void
{
$post = Post::find($this->postId);
if (! $post) return;
$broadcaster->toChannel("post.{$post->id}", ['event' => 'post.updated', 'body' => $post->toArray()]);
}
}
Broadcast to many channels in one request. The library supports publishing to multiple channels per call — use it for notification fan-out (e.g. all members of a group) instead of looping HTTP calls:
$channels = $group->members->map(fn ($u) => "user.{$u->id}")->all();
$client->publish($channels, ['event' => 'group.mention', 'group' => $group->id]);
Conventional channel naming. Pick a scheme once and stick to it: user.{id} for per-user feeds, post.{id} for resource rooms, private-user.{id} where Centrifugo namespace rules require prefixed channels. Keep the naming logic in one helper so backend and frontend agree.
Testing. Because the library makes real HTTP calls, don’t hit it in tests. Either (a) swap the singleton for a fake in TestCase::setUp() / a Mockery spy, or (b) if the client is Guzzle-based and accepts a client in its constructor, inject a GuzzleHttp\Handler\MockHandler. Faking the concrete class is brittle, which is another reason to depend on App\Services\Broadcaster in production code.
Console/scheduler integration. Nothing special is required — resolve the client via the container inside commands ($this->laravel->make(Client::class)) or type-hint it in handle(). Useful for periodic digests, cache warmers, or a centrifugo:ping health command.
POST /api with Authorization: apikey <key> (v1/v2 era). Centrifugo v4/v5 introduced X-API-Key and stricter JSON shape changes. Verify your server version and, if mismatched, either pin the server or skip the library and call the endpoint with Laravel’s Http client. Confirm the class name and method signatures in vendor/ before writing code — READMEs for small 2018 packages are often stale relative to the tagged source.try/catch for transport/HTTP errors and log with channel + event context. Never let a failed publish abort a user request that already succeeded in the database.only([...])) rather than sending toArray() of large relations; large payloads hurt every connected client.connect/subscribe proxies or JWT. Building a private- channel in Laravel does nothing unless Centrifugo is configured to enforce it..env/secret manager. If it leaks to a JS bundle, anyone can publish to any channel.Http::withHeaders(['X-API-Key' => ...])->post("{$url}/api", ['method' => 'publish', ...]) is often easier to maintain than adapting an eight-year-old client — and it plays nicely with Http::fake() in tests.How can I help you explore Laravel packages today?