php-standard-library/channel
Message-passing channels for async communication in PHP, inspired by Go and Rust. This package brings typed, concurrent channel primitives to the PHP Standard Library, enabling clean producer/consumer workflows.
Verdict: Narrow but legitimate primitive; pilot-only, not a platform standard. The package is a Go/Rust-style channel abstraction (buffered/unbuffered, blocking/non-blocking send/receive, foreach consumption, explicit close) built on PSL async primitives. It solves a real problem — coordinating concurrent work inside a single PHP process without shared mutable state — but the evidence base for adoption is thin and the surrounding pitch material overstates what it can do.
Architecture fit
| Dimension | Assessment |
|---|---|
| Core capability | In-process, in-memory message passing between fibers/coroutines. Correct tool for pipelines, worker pools, producer/consumer handoff, and backpressure inside one event loop. |
| Laravel fit | Low-to-neutral. No service provider, no container bindings, no facade, no config. It is a library, not a Laravel package — all glue is ours. |
| Async runtime dependence | Only meaningful under Swoole, RoadRunner, ReactPHP/Revolt, or Amp. Under PHP-FPM it degrades to blocking calls on the request worker, which is worse than doing nothing. |
| Overlap with existing stack | Competes conceptually with Illuminate\Bus, Events, and Queue. It is not a substitute for any of them: it has no persistence, no retries, no cross-process delivery, no dead-letter handling. |
Critical correction to the business case. The product narrative supplied with this package claims channels can replace HTTP calls between Laravel microservices (e.g., OrderService → InventoryService). That is technically wrong. These are in-memory channels; they cannot cross a process, container, or host boundary. Any pitch built on "replace inter-service HTTP with channels" will fail at implementation and damage credibility. The defensible value proposition is strictly intra-process: replacing ad-hoc shared state, nested callbacks, or polling loops inside a single long-lived worker.
Integration feasibility. Feasible, but with real friction:
receive() on a channel whose producer died hangs the worker indefinitely. We must build timeouts ourselves or accept hard worker restarts as the failure mitigation.Supply and maturity risk (the dominant risk).
php-standard-library/php-standard-library monorepo; per-package release notes are literally "see the main repository." Change tracking, security review, and upgrade impact analysis therefore require following the monorepo, not this repo.Key questions for the maintainers / spike
send/receive yield the fiber (cooperative) or block the OS thread? Which loop implementations are tested?ext-swoole coupling?close() the only escape hatch? What happens to buffered-but-unread messages on close()?Stack fit. Only pursue this where we already run (or are committed to running) a persistent async runtime. Concretely: Swoole/RoadRunner workers, Octane-hosted services, queue workers that keep a process alive, or CLI daemons. Exclude anything served by classic PHP-FPM — no exceptions, since a blocking channel in a request lifecycle consumes a worker with no yield.
Compatibility. Assume a Laravel 10/11+ baseline on PHP 8.2+ for fibers, with the async runtime treated as a hard prerequisite. Validate against our actual Octane/Swoole version in the spike rather than trusting the README. Also confirm behavior under our observability stack (Telescope, Sentry) — fibers obscure stack traces and a hung worker produces no exception to report.
Architecture pattern — wrap before use. Do not let channel types leak into domain or application code. Introduce an internal contract (e.g., App\Support\Concurrency\MessageBus with send/receive/close) with two adapters: a channel-backed implementation and a no-op/synchronous implementation for the FPM path and for tests. This buys three things: a one-file swap if the package stalls, a place to inject timeouts and metrics that upstream lacks, and a path to route the same code over a durable transport later.
Migration path. There is no migration from queues and no migration from HTTP calls — neither is in scope. The only legitimate migration is replacing hand-rolled intra-worker coordination (static arrays, polling flags, sleep loops, shared mutable state in Octane workers). Inventory those patterns first; if the count is near zero, stop here.
Sequencing
| Phase | Scope | Exit criteria | Effort |
|---|---|---|---|
| 0. Diligence | Answer the six questions above; verify release cadence and PHP/ext requirements; read the source of BufferedChannel/UnbufferedChannel (~small surface) |
No blocking unknowns on timeout/close/buffer semantics | 2–3 days |
| 1. Wrapper spike | Build the MessageBus contract + channel adapter + timeout/metrics layer; unit and fork-based deadlock tests |
Wrapper passes a hung-consumer test without killing the process | 1–2 weeks |
| 2. Single-workload pilot | One non-critical, non-durable workload — e.g., in-worker fan-out for log/event aggregation, or a bounded image/PDF worker pool inside one Octane worker | Measurable latency/throughput win vs. current approach; zero hung-worker incidents over 2 weeks | 2–4 weeks |
| 3. Gate review | Compare against the alternative: plain fibers/Fiber::suspend() or the runtime's native channel (Swoole\Channel), which carry no third-party risk |
Written go/no-go with an owner | 1 day |
| 4. Expand (conditional) | Additional intra-worker pipelines only | — | ongoing |
Phase 3 is the real decision point. Swoole\Channel already ships with the runtime we'd need anyway, and plain fibers cover most simple producer/consumer shapes. The package must demonstrate value over those, not merely over queues.
Maintenance. Low line-count, low-friction code, but the maintenance burden is relational, not technical: we are adopting a near-zero-adoption dependency from a single monorepo. Assign an explicit owner (one team, one named engineer). Pin the version exactly in composer.json, mirror via our private registry, and review monorepo release notes — not this repo's — before any bump. Budget a small recurring allowance for patching a fork if upstream goes quiet; MIT plus a small API surface makes this tractable.
Support. No vendor, no SLA. Upstream support is GitHub issues on the monorepo, competing with the rest of PSL for maintainer attention. Internally, support means: a wrapper with a documented contract, a runbook, and a named escalation path. Do not let this reach production without the runbook existing first.
Scaling. The scalability model is unusual and must be stated plainly to stakeholders: this does not scale a service; it makes a single process more efficient. Throughput gains come from eliminating I/O round-trips inside one worker. Horizontal scale is unchanged, and there is a hard ceiling at one CPU core per process — PHP offers no true parallelism here, so CPU-bound work gains nothing and can starve the consumer. Channel buffers do not increase throughput, only decouple timing; oversized buffers convert a latency problem into a memory problem.
Failure modes and mitigations
| Failure | Symptom | Mitigation |
|---|---|---|
Consumer dies; producer blocks on send |
Silent worker hang, no exception, health check may still pass | Wrapper-enforced send timeout; liveness probe on worker heartbeat, not just HTTP /health |
| Unbounded / oversized buffer under burst | Gradual memory growth to OOM, often in the largest worker | Hard buffer cap + memory_get_peak_usage() alarm; non-blocking send with explicit drop/escalate policy |
| Message loss on crash, restart, or deploy | Work silently never completes; no error surface | Restrict to non-durable, idempotent, best-effort workloads only — document this per use case |
| Ungraceful shutdown drains nothing | In-flight buffered messages lost on deploy | Signal handling: stop accepting, drain channels, then exit; add to the deploy checklist |
| Closed-channel operations | Exceptions on send after close | Explicit close ownership convention (producer closes, consumer observes) enforced in review |
| Fiber stack traces in error tracking | Debugging time balloons | Verify Sentry/Telescope fiber support in the spike; log correlation IDs through the wrapper |
Ramp-up. The real cost. Async PHP competency (fibers, cooperative scheduling, deadlock reasoning) is uncommon on a typical Laravel team, and the package ships no Laravel-oriented examples. Plan for: a half-day internal walkthrough, a short code-review checklist (buffer sizing rationale, close ownership, timeout present, idempotency), and a pairing requirement for the first two production usages. Expect 3–6 weeks before a mid-level engineer is independently productive, and assume on-call needs a deadlock triage runbook — because the most likely pager at 2 a.m. is "worker stuck, no error," not "worker threw."
Bottom line for the roadmap. Approve a scoped spike and a single non-critical pilot behind an internal abstraction. Do not approve it as a platform dependency, a queue replacement, or an inter-service transport, and do not let the "replace microservice HTTP calls" framing into any stakeholder deck — it is the fastest way to lose engineering trust in this evaluation.
How can I help you explore Laravel packages today?