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

Pubcontrol Laravel Package

fanout/pubcontrol

fanout/pubcontrol is a PHP PubControl client for publishing messages to Fanout Cloud and GRIP-compatible services. It provides a simple API for channels, message formats, and HTTP publishing.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin.

composer require fanout/pubcontrol

The package is archived and unmaintained, so treat Composer as a snapshot tool: commit composer.lock, and if you're on Laravel 9+ verify that its pinned Guzzle constraint resolves (see Gotchas).

Where to look first.

  1. src/PubControl/PubControlClient.php — the HTTP transport. This is where auth (set_auth_key, set_auth_jwt, set_ca_file), the endpoint URI, and the actual POST live. Read this before anything else; almost every problem you'll hit is here.
  2. src/PubControl/Item.php — the {'formats': {...}} envelope, optional id and prev-id for message ordering.
  3. src/PubControl/Format/* — payload shapes: HttpStreamFormat, HttpResponseFormat, JsonObjectFormat.
  4. src/PubControl/PubControl.php — thin aggregator over clients; add_client, get_client, publish.

First use case: publish one item to one channel.

use PubControl\PubControl;
use PubControl\PubControlClient;
use PubControl\Item;
use PubControl\Format\HttpStreamFormat;

$pub = new PubControl();
$pub->add_client(new PubControlClient('https://api.fanout.io/realm/{realm}'));
$pub->get_client(0)->set_auth_key(config('services.fanout.key'));

$pub->publish('orders', new Item(new HttpStreamFormat(json_encode(['id' => 42]))));

If that POST returns 2xx and your subscriber sees the frame, you've validated the whole stack: endpoint, auth, item envelope, format. Everything after this is plumbing.

Implementation Patterns

Bind it as a singleton, don't new it per request. Constructing a PubControl per publish is cheap but re-auths and re-reads config every time. Register a factory in a service provider:

$this->app->singleton(PubControl::class, function () {
    $pub = new PubControl();
    $pub->add_client(new PubControlClient(config('services.fanout.uri')));
    $pub->get_client(0)->set_auth_key(config('services.fanout.key'));
    return $pub;
});

Wrap it in a small Publisher class (publish(string $channel, Item $item)) so you can swap implementations when this package inevitably dies.

Format selection is the main design decision.

Format Use when
HttpStreamFormat Subscribers use streaming transports (EventSource/WebSocket through Fanout)
HttpResponseFormat Subscribers use long-polling or your backend does GRIP response hold
JsonObjectFormat Transport-agnostic JSON payloads, typically wrapped inside a stream format

Most PHP web apps land on HttpStreamFormat with a JsonObjectFormat-style JSON body and an explicit content type:

$stream = new HttpStreamFormat(json_encode($payload));
$stream->set_content_type('application/json');

Publish from domain events, not controllers. The idiomatic Laravel shape is a listener on an event (OrderShipped, MessagePosted) that queues a PublishToChannel job. Publishing inline in a controller couples request latency to a third-party HTTP call — see Gotchas.

Use id / prev-id if clients need ordering. EPCP supports gap detection; set new Item($format, $id, $prevId) and let clients detect dropped messages. Skipping this is fine for fire-and-forget notifications, not for state sync.

Self-hosted Fanout vs Fanout Cloud. Self-hosted is a plain HTTP endpoint (http://localhost:5561/); Cloud uses https://api.fanout.io/realm/{realm} with auth. Keep the URI in config/services.php so the same code runs both ways. Note the trailing slash — the client concatenates rather than resolves.

Migrate toward fanout/php-gripcontrol for new work. GripPubControl extends the same PubControl core and adds GRIP-specific helpers (publish_http_response, publish_websocket, publish_grip). If your integration grows past "publish a message", you want that package, not this one.

Test without the network. Because PubControl is a thin aggregator, fake at your Publisher boundary in tests and assert on channels/items, rather than mocking Guzzle internals. Keep one integration test that hits a real or stubbed endpoint.

Gotchas and Tips

Archived means abandoned. No security fixes, no PHP 8.2+ deprecation cleanup, no Guzzle 7 support in older tags. Check the resolved Guzzle version before adopting; Laravel 10/11 apps ship Guzzle 7 and older pubcontrol releases may conflict. If you must use it, isolate it behind your Publisher interface so a rewrite is a one-file change.

Publishing is synchronous by default. publish() without a callback performs a blocking HTTP request and throws on transport failure. In a web request, that's added latency and a failure mode. Always dispatch through a queued job, and always try/catch — there's no built-in retry, backoff, or circuit breaking.

Async callbacks are Guzzle-version-sensitive. The callback path (publish($channel, $item, $callback)) relies on the underlying client's promise behavior. Don't assume the callback fires with a boolean in every version; assert your error handling against the actual resolved source. Queuing is more predictable than async here.

Auth: key vs JWT. set_auth_key() sends HTTP Basic with the base64 realm key — simplest and correct for server-to-server. set_auth_jwt() requires firebase/php-jwt (a soft dependency; it will fail at runtime, not install time, if absent) and is only needed for scoped/expiring tokens. Don't mix them on the same client.

Item envelope shape. The wire payload is an items array, each item holding a formats map — not a single format. If you're hand-rolling requests for debugging, replicate that exactly or the server 400s without a useful message.

Silent-ish failures. Non-2xx responses surface as exceptions from the transport, and the message is often just a status code. Log the endpoint URI (without the key), channel, and the response body when debugging. A 401 almost always means a wrong realm key; a 404 usually means a malformed realm path or missing trailing slash.

HttpStreamFormat defaults. Content type defaults to text/plain. If you push JSON and don't set application/json, browsers and clients will parse it as text. Set it explicitly, and remember close affects whether the stream terminates after the frame — usually you want it false.

CA bundles and proxies. set_ca_file() exists for a reason: on older curl/OpenSSL setups behind corporate proxies, TLS verification failures masquerade as connection errors. Bind the CA path through config rather than hardcoding.

Extension point. PubControl\Format\FormatInterface (or the equivalent base in your version) is the seam: subclass it to add a custom format without touching the client. This is the only part of the package worth designing around.

The real long-term tip: Fanout Cloud is end-of-life and this library is the thinnest possible client around a single HTTP POST. If you're greenfield, the honest alternative is a 40-line Guzzle call to the same EPCP/GRIP endpoint, which removes an unmaintained dependency entirely. Adopt this package only as a legacy-compatibility shim with an explicit exit plan.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums