waffle-commons / runtime
The SAPI-agnostic runtime for Waffle Commons: boots a kernel and emits responses through contracts interfaces only, so the same application runs under FrankenPHP worker mode, PHP-FPM or the CLI server.
Requires
- php: ^8.5
- psr/container: ^2.0
- psr/http-message: ^1.1 || ^2.0
- waffle-commons/contracts: 0.1.0-beta6
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.7.0
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 20:25:32 UTC
README
Waffle Runtime Component
Release:
0.1.0-beta6Β |ΒCHANGELOG.md
WaffleRuntime is the agnostic application runner. It owns the request loop in FrankenPHP worker mode and falls back gracefully to a single-shot execution under the classic PHP SAPI when frankenphp_handle_request() is unavailable.
The runtime contains no concrete framework dependencies β it knows only the contracts interfaces KernelInterface, ResponseEmitterInterface, and GlobalsFactoryInterface. The concrete GlobalsFactory / ResponseEmitter (from http) are injected by the application.
π¦ Installation
composer require waffle-commons/runtime
π§± Surface
A single class: Waffle\Commons\Runtime\WaffleRuntime implementing Waffle\Commons\Contracts\Runtime\RuntimeInterface.
public function __construct( GlobalsFactoryInterface $globalsFactory, // required β wired by the app bootstrap ResponseEmitterInterface $emitter, // required β wired by the app bootstrap ); public function loop(KernelInterface $kernel, int $maxRequests = 500): void;
π Bootstrap (the entire public/index.php)
<?php declare(strict_types=1); use Waffle\Commons\Http\Emitter\ResponseEmitter; use Waffle\Commons\Http\Factory\GlobalsFactory; use Waffle\Commons\Runtime\WaffleRuntime; use App\Factory\AppKernelFactory; require __DIR__ . '/../vendor/autoload.php'; define('APP_ROOT', dirname(__DIR__)); $kernel = AppKernelFactory::create(env: getenv('APP_ENV') ?: 'prod', debug: false); // The app wires the concrete http factory + emitter into the agnostic runtime. (new WaffleRuntime(new GlobalsFactory(), new ResponseEmitter()))->loop($kernel, maxRequests: 500);
π The loop contract
- Boot once.
$kernel->boot()->configure()runs exactly once when the FrankenPHP worker starts. - Iterate. Up to
$maxRequeststimes, the runtime callsfrankenphp_handle_request($handler)where$handler:- rebuilds a PSR-7
ServerRequestfrom the current superglobals (FrankenPHP repopulates them per request), - calls
$kernel->handle($request)(the hot path), - emits the response via the injected
ResponseEmitterInterface.
- rebuilds a PSR-7
- Garbage-collect periodically. Every 50 requests,
gc_collect_cycles()is called to keep long-running worker memory bounded. - Reset on exit. When the loop exits (max reached or FrankenPHP signaled stop),
$kernel->reset()clears request-scoped state.
If frankenphp_handle_request is not defined (classic SAPI), the runtime executes the handler once and exits β no infinite loop.
π PHP 8.5 features used
final class WaffleRuntimeβ no inheritance.- Typed constructor parameters injected via
contractsinterfaces (no concrete defaults). - First-class callable closure in the handler block.
- Typed
KernelInterface+ResponseEmitterInterface+GlobalsFactoryInterfacedependencies.
π§ 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\Runtime may depend only on:
Waffle\Commons\Runtime\**β itselfWaffle\Commons\Contracts\**β the shared contracts package, the only Waffle dependencyPsr\**β PSR interfaces (PSR-7 / PSR-17)@global+Psl\**β PHP core (including the FrankenPHPfrankenphp_handle_requestglobal) and the PHP Standard Library
Test code under WaffleTests\Commons\Runtime is unrestricted (@all); WaffleRuntimeWorkerModeTest is listed in [guard].excludes because it re-declares the production namespace to stub frankenphp_handle_request. 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 ad-hoc through one another.
π§ͺ Testing
docker exec -w /waffle-commons/runtime waffle-dev composer tests
The WaffleRuntimeWorkerModeTest namespaces the production namespace to override frankenphp_handle_request via php-mock-phpunit; it is listed in mago.toml [guard].excludes.
π Documentation
Central framework docs (DiΓ‘taxis) for this component:
- Reference:
reference/runtime.md - Explanation:
explanation/performance.md - Full documentation tree: waffle-commons/documentation
π License
MIT β see LICENSE.md.