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.
Architecture fit. fanout/php-pubcontrol is a thin, single-purpose client for EPCP (Extensible Pubsub Control Protocol) — it serialises Item/Format objects and POSTs them to a Fanout-compatible publish endpoint over Guzzle. It is not a self-contained realtime stack. The half of the architecture that actually matters — a GRIP-aware edge/proxy that intercepts responses, holds connections open (Grip-Hold: stream / response), and subscribes clients to channels (Grip-Channel) — lives entirely outside this package. In a Laravel app, this library only replaces the "publish a message" line of code. Anyone evaluating it expecting a Pusher/Ably-style SDK with an in-app server will be disappointed; the correct mental model is "a Guzzle wrapper that emits an authenticated HTTP instruction to a third-party edge."
Integration feasibility. Mechanically straightforward but not turnkey. There is no Laravel service provider, no facade, no config file, and no published configuration — you would register a container singleton yourself in AppServiceProvider, read the realm/URI/key from config/services.php (and .env), and wrap the client in a queued job so publishing does not sit on the request path. The API is small (PubControlClient, Item, and the format classes such as WebsocketContext, HttpStreamResponse, HttpResponse, GripInstruct), so the surface to wrap is genuinely tiny — on the order of a day of glue work, not a sprint. The friction is that publish calls return Guzzle promises; in a synchronous Laravel job you must ->wait() or settle them explicitly, and the README-era idioms predate Laravel's HTTP client and modern Guzzle async patterns.
Technical risk. This is the dominant concern, and it is severe. The repository is archived, has 3 stars, and 0 dependents — there is no evidence of a production user base to lean on when something breaks. An archived package means no security patches, no Guzzle 7+ compatibility guarantees, no PHP 8.x guarantees beyond what happens to work, and no maintainer to escalate to. The larger risk is existential rather than code-level: the package is only useful against a live Fanout/Fastly Fanout publish endpoint. If your organisation is not already a Fanout customer with a provisioned realm and an edge configured for GRIP, the library has no destination — evaluating it without confirming service availability and commercial terms first is the classic way this kind of dependency turns into a dead end. Secondary risks: credentials are passed per-client at construction time (easy to leak into logs or misconfigure per-environment), the library does no retry, backoff, or circuit breaking, and no GRIP signature verification is included here (that lives in a separate library), so inbound webhook/stream authenticity is your responsibility.
Key questions. Is the organisation a current Fanout/Fastly Fanout customer, and is that commitment durable for the expected lifetime of the feature? Who owns the GRIP-capable edge, and does it already exist? Does the feature genuinely need GRIP-style long-lived streaming, or would a first-party Laravel stack (Reverb, Soketi, Pusher-protocol) satisfy it with a maintained dependency? Has anyone verified the library runs clean on the target PHP and Guzzle versions in CI? What is the exit cost if the edge service is retired — is the publish call isolated behind an interface so it can be swapped?
Stack fit. Poor-to-conditional. Laravel's ecosystem has converged on the Pusher protocol (Laravel Echo, broadcasting drivers, Reverb as a first-party server), and this package fits none of it — there is no broadcast driver, no Echo compatibility, no event broadcasting hook. Adopting it means either hand-rolling a broadcast driver that maps Laravel events to Item/Format objects, or bypassing broadcasting entirely and publishing imperatively from service classes and jobs. It fits only if the product specifically needs GRIP semantics (HTTP response holding, long-poll fallbacks, websocket-over-HTTP at the CDN edge) — a genuinely different model from channel-based pub/sub, and one that buys you edge delivery at the cost of Laravel idiom.
Migration path. Adopt behind a seam, never as a direct dependency. Define a narrow RealtimePublisher interface in your domain (publish(string $channel, array $payload): void), implement it once with PubControl, and bind it in the container. Wrap the implementation in a queued job with tries/backoff and explicit promise resolution so publish failures surface as job failures rather than silent drops. Keep configuration in config/services.php with credentials in .env, and inject the client as a singleton rather than constructing PubControlClient per call. If you already have a Pusher-style abstraction, the migration is "add a second driver," which is cheap; if you are migrating off something, run both paths behind a feature flag per channel.
Compatibility. Assume nothing. Pin the package version, then verify — in CI, on your actual PHP and Guzzle versions — that the promise chain resolves and that format serialisation matches what your edge expects. Because the package is archived, treat Composer resolution as a one-time event: commit composer.lock, and if you must move Guzzle forward later, expect to fork and patch rather than upgrade. Vendor the fork under a private namespace if the dependency survives past a quarter.
Sequencing. (1) Confirm edge/service availability and commercial terms — this is the gate; do not write code before it clears. (2) Prove the end-to-end path in a spike: publish one Item to one channel, observe it arrive through a GRIP-configured endpoint. (3) Land the interface, config, and container binding with a no-op implementation so the app compiles. (4) Implement and test the PubControl driver in a queued job with failure handling. (5) Add observability — publish latency, failure rate, promise rejections. (6) Only then wire real product features, channel by channel.
Maintenance. Zero upstream maintenance available, by definition — archived means the security-patch cadence is "never." Your team owns the code from day one, including any Guzzle or PHP-version drift, and the practical mitigation is to plan for a maintained fork or a rewrite rather than pretending an upgrade path exists. Budget a small but permanent line item: someone must be able to read and patch roughly a few hundred lines of HTTP-serialisation code. The compensating factor is the small surface area — this is not a framework you are inheriting.
Support. No vendor support for the library, and the surrounding service support depends entirely on the commercial relationship with Fanout/Fastly. Internally, support load concentrates in one place: whoever understands GRIP. That is a single point of knowledge failure and should be documented as such, with a written runbook covering credentials, endpoint configuration, and what to do when publishes fail.
Scaling. Publish throughput scales trivially — it is outbound HTTP, so you scale it the way you scale any queue worker pool, and the edge absorbs the fan-out. The scaling limits live in the edge service: connection counts, channel cardinality, and message rate are governed by the Fanout plan, not by this package. The Laravel-side scaling concern is queue depth during traffic spikes, since every publish is an outbound round trip; batching multiple Items per call and keeping concurrency bounded matters more than any client tuning.
Failure modes. Publish is fire-and-forget over HTTP, so the default failure mode is silent message loss — a rejected promise that nobody checks means a user never sees an update and no alarm fires. Others: edge unavailable or misconfigured (all publishes 4xx/5xx, app appears healthy); credential rotation breaking the singleton; promise never settled, hanging a worker until timeout; duplicate delivery on job retry with no idempotency key, causing double-posted messages; and the strategic failure mode — the service is retired or the plan lapses and the realtime feature stops with no code change on your side. Every one of these argues for explicit promise settlement, retries with backoff, a dead-letter path, and per-channel feature flags that can disable realtime without a deploy.
Ramp-up. Low for the library, moderate-to-high for the concept. A mid-level Laravel developer can read the entire API in under an hour; understanding GRIP — hold modes, channel subscription headers, the interaction between the edge and your origin responses, and where signature verification belongs — is a multi-day learning curve with thin public documentation and essentially no community to ask. Plan for one engineer to become the designated owner, do the spike, and write the internal note. Given the archived status and near-zero adoption, the honest TPM recommendation is: proceed only if the Fanout/Fastly edge is already a committed part of the architecture, isolate it behind an interface from the first commit, and hold a named exit plan — otherwise, a maintained Pusher-protocol alternative is the lower-risk path for the same product outcome.
How can I help you explore Laravel packages today?