Download the PHP package waffle-commons/async without Composer
On this page you can find all versions of the php package waffle-commons/async. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download waffle-commons/async
More information about waffle-commons/async
Files in waffle-commons/async
Package async
Short Description Fiber-based finish-request deferred task runner for Waffle Commons: short post-response work lifted out of the user-perceived latency path, under a bounded per-request budget.
License MIT
Informations about the package async
Waffle Async Component
Release:
0.1.0-beta6|CHANGELOG.mdRFC: RFC-015 (ASYNC-01) — Fiber-based finish-request task deferral
A bounded, worker-safe runner that lifts short post-response work — mail delivery, webhook fan-out, audit
writes — out of the user-perceived latency path. Tasks deferred during a request run after the response is
flushed to FrankenPHP but before the worker accepts its next request, each inside its own native Fiber for
failure isolation. This is finish-request deferral, not background processing: the work runs on the same
single worker thread, sequentially, under a hard per-request budget.
The concurrent half of RFC-015 — outbound HTTP fan-out via
ConcurrentClientInterface— ships inwaffle-commons/http-client, joined to this package only throughwaffle-commons/contracts.
📦 Installation
🧱 Surface
| Class | Role |
|---|---|
Waffle\Commons\Async\DeferredTaskRunner |
final runner implementing TaskRunnerInterface and ResettableInterface (declared directly, as igor-php requires). Holds the request-scoped pending queue. |
Waffle\Commons\Async\Exception\DeferralBudgetExceededException |
Thrown by defer() once the per-request budget is exhausted. Implements the contract DeferralBudgetExceededExceptionInterface; exposes budget(): int. |
Waffle\Commons\Async\Exception\InvalidBudgetException |
Thrown at construction when $budget < 1. Implements the contract AsyncExceptionInterface; exposes budget(): int. |
The contracts — Waffle\Commons\Contracts\Async\TaskRunnerInterface, DeferredTaskInterface,
Exception\AsyncExceptionInterface, Exception\DeferralBudgetExceededExceptionInterface — live in
waffle-commons/contracts. Consumers (controllers, the kernel) depend on those interfaces, never on this
concrete package.
🚀 Usage
⏱️ The runner
- Budget guard. A
$budgetbelow1throwsInvalidBudgetExceptionat construction — a budget that rejects every deferral is a programming error, refused eagerly.DEFAULT_BUDGETis64. defer()appends to a request-scopedlist<DeferredTaskInterface>; oncepending() >= $budgetit throwsDeferralBudgetExceededExceptioninstead of enqueuing — the explicit signal to move the workload to a real queue or broker.run()snapshots the pending queue and clears it before draining, so a task that itself defers another task cannot grow the queue being drained, and the queue is empty even if the drain throws.- Fiber lifecycle, per task. The runner
start()s the task'sFiber; if it is not terminated it isresume()d exactly once — the single cooperative resume, there is no scheduler loop. If it is still not terminated it is abandoned with awarninglog. The Fiber is then force-destroyed (unset) inside the sametry, so a throwing destructor/finallyis caught and logged rather than detonating as a fatal that would abort the sibling tasks. - Failure isolation. Any
\Throwablefrom a task is caught and logged aterrorlevel usingDeferredTaskInterface::name()as the label; siblings continue running. reset()empties the pending queue — the only request-scoped state — so deferred work never bleeds across the FrankenPHP worker boundary.
🧵 Worker safety
DeferredTaskRunner's pending queue is the only request-scoped state. It implements ResettableInterface
directly (igor-php requires the direct declaration, not one inherited transitively via
TaskRunnerInterface), and the kernel calls reset() after every request so no deferred work ever crosses
the worker boundary (wfl igor 0 KO). Fibers here are a failure-isolation boundary, not a thread pool —
there is exactly one OS thread; two deferred tasks never run simultaneously, they run one after another
during finish-request.
Finish-request deferral is deliberately not background processing: it trades a slice of worker throughput for perceived latency, bounded by the hard per-request budget rather than a wall-clock timeout. Routinely wanting to defer dozens of tasks is the tripwire that the work belongs on a real queue/broker instead, not in this budget.
🔌 Wiring (kernel package)
The finish-request drain itself is not in waffle-commons/async — the package stays contracts-only. The
kernel package wires:
Waffle\Event\Listener\DeferredTaskFlushListener(final readonly) — subscribed to the kernel'sTerminateEvent(fires after the response is flushed); its__invoke()calls$runner->run()inside a catch-all, so a throwable escaping the drain cannot reach the client.- The template
AppKernelFactoryregistersDeferredTaskRunnerunderTaskRunnerInterface::classwithDeferredTaskRunner::DEFAULT_BUDGET, and adds the flush listener onTerminateEvent.
🐘 PHP 8.5 features used
final class DeferredTaskRunnerwith promoted,readonlyconstructor properties ($budget,$logger).- Native
Fiberas a per-task isolation boundary (start()/resume()/isTerminated()). public const int DEFAULT_BUDGET— typed class constant.#[\Override]on every interface implementation.
🧭 Architectural boundary (mago guard)
An active dependency perimeter is enforced on every CI run by vendor/bin/mago guard (bundled into
composer mago; zero baselines). The rules live in mago.toml under [guard.perimeter] — a
forbidden use statement fails the build, not a reviewer.
Production code under Waffle\Commons\Async may depend only on:
Waffle\Commons\Async\**— itselfWaffle\Commons\Contracts\**— the shared contracts package, the only Waffle dependency permittedPsr\**— PSR interfaces (psr/log)@global+Psl\**— PHP core (including nativeFiber) and the PHP Standard Library
Test code under WaffleTests\Commons\Async is unrestricted (@all). Structural rules are guarded too:
interfaces must be named *Interface, Exception\** classes must end in *Exception, and any Enum\**
namespace may hold only enum declarations.
Contract-first, component-agnostic by construction: components compose through waffle-commons/contracts,
never directly through one another.
🧪 Testing
📚 Documentation
Central framework docs (Diátaxis) for this component:
- Reference:
reference/async.md - Explanation:
explanation/async-finish-request-deferral.md - Full documentation tree: waffle-commons/documentation
📄 License
MIT — see LICENSE.md.