waffle-commons / waffle
A modern, minimalist, and security-focused PHP micro-framework.
Requires
- php: ^8.5
- psr/cache: ^3.0
- psr/container: ^2.0
- psr/event-dispatcher: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
- waffle-commons/contracts: 0.1.0-beta2.1
- waffle-commons/utils: 0.1.0-beta2.1
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-05-30 19:46:59 UTC
README
Waffle โ the Kernel
Release:
v0.1.0-beta2ย |ยCHANGELOG.md
The application kernel. Orchestrates request handling against the PSR-15 middleware stack, dispatches RequestReceivedEvent / ResponseGeneratedEvent / TerminateEvent, and resolves controllers via the container. The kernel itself stays agnostic of routing, security, logging, and HTTP โ every concrete dependency is injected.
๐ Beta-1 highlights
- Kernel decoupling.
AbstractKernel::handle()resolves the terminal handler from the container underPsr\Http\Server\RequestHandlerInterfaceโ there is no hard-codednew ControllerDispatcher(...)on the hot path.configure()registers a defaultControllerDispatcheronly when the slot is empty (has()-gated, idempotent), so an app can swap in its own terminal handler by pre-registering one. - Native DTO validation โ 422.
ControllerArgumentResolverhydrates#[Dto]-tagged parameters from the parsed request body, letting PHP 8.5 set property hooks run their assertions during construction. A hook failure is trapped and re-thrown as a unifiedValidationException, which the error handler renders as RFC 7807422 Unprocessable Entityโ closing the Mass-Assignment gap without any external validation package.
๐ฆ Installation
composer require waffle-commons/waffle
๐งฑ Surface
| Class | Role |
|---|---|
Waffle\Kernel |
Concrete kernel. Extends AbstractKernel. |
Waffle\Abstract\AbstractKernel |
Implements KernelInterface::boot/configure/handle/reset; provides DI setters for setContainerImplementation, setConfiguration, setSecurity, setMiddlewareStack, setEventDispatcher. |
Waffle\Abstract\AbstractController |
Convenient base for app controllers; provides PSR-7 helper accessors. |
Waffle\Abstract\AbstractSystem |
Internal lifecycle binder used by Waffle\Core\System. |
Waffle\Core\System |
Boots the security / container linkage once the kernel reaches configure(). |
Waffle\Core\BaseController |
Default BaseControllerInterface implementation. |
Waffle\Handler\ControllerDispatcher |
Terminal PSR-15 handler that the middleware stack falls through to. Calls the controller method resolved from _controller and _route_params. |
Waffle\Handler\ControllerArgumentResolver |
Hydrates controller parameters, including #[Dto]-tagged DTOs from the parsed body. |
Waffle\Handler\ControllerResponseConverter |
Converts a controller's scalar / array return into a PSR-7 ResponseInterface. |
Waffle\Exception\* |
Domain exceptions (WaffleException, RouteNotFoundException, RenderingException, ValidationException, InvalidConfigurationException). |
๐ Composing a kernel
use Waffle\Kernel; use Waffle\Commons\Container\Container; use Waffle\Commons\Config\Config; use Waffle\Commons\Pipeline\MiddlewareStack; use Waffle\Commons\Security\Security; use Waffle\Commons\EventDispatcher\Dispatcher\EventDispatcher; use Waffle\Commons\EventDispatcher\Provider\ListenerProvider; use Waffle\Commons\Log\StreamLogger; $config = new Config(__DIR__ . '/config', getenv('APP_ENV') ?: 'prod'); $container = new Container(); $container->set(ConfigInterface::class, $config); $kernel = new Kernel(new StreamLogger()); $kernel->setConfiguration($config); $kernel->setContainerImplementation($container); $kernel->setSecurity(new Security($config)); $kernel->setMiddlewareStack((new MiddlewareStack()) ->add(new ErrorHandlerMiddleware($renderer, $logger)) ->add(new TrustedHostMiddleware($config->getArray('waffle.trusted_hosts', []) ?? [])) ->add(new CoreRoutingMiddleware($router)) ->add(new SecurityMiddleware($kernel->security)) ); $kernel->setEventDispatcher(new EventDispatcher(new ListenerProvider()));
The setter signatures, verbatim from src/Abstract/AbstractKernel.php:
public function setContainerImplementation(PsrContainerInterface $container): void; public function setConfiguration(ConfigInterface $config): void; public function setSecurity(SecurityInterface $security): void; public function setMiddlewareStack(MiddlewareStackInterface $stack): void; public function setEventDispatcher(EventDispatcherInterface $dispatcher): void;
The constructor takes a PSR-3 logger only (defaults to NullLogger):
public function __construct(protected LoggerInterface $logger = new NullLogger())
๐ Request lifecycle
handle(ServerRequestInterface)ensuresboot()+configure()have run (idempotent โ the$bootedguard short-circuits subsequent calls).validateState()rejects an unconfigured kernel withRuntimeException/ContainerException/NotFoundExceptionas appropriate.RequestReceivedEventis dispatched; listeners may swap the request.- The middleware stack runs; the terminal handler is a
ControllerDispatcherresolving the route's_controller+_route_params. ResponseGeneratedEventis dispatched; listeners may swap the response.- The response is returned to
WaffleRuntimewhich emits it and callsterminate()for post-emission listeners. reset()clears request-scoped state (container reset).
๐จ Built-in events
Waffle\Event\RequestReceivedEventโ fires before the middleware pipeline runs.Waffle\Event\ResponseGeneratedEventโ fires after the pipeline returns.Waffle\Event\TerminateEventโ fires after the response is emitted (for heavy async work).
All three are PSR-14 events; RequestReceivedEvent and ResponseGeneratedEvent are not stoppable โ they expose mutator methods (getRequest(), getResponse()) so listeners can replace the message.
๐ PHP 8.5 features used
protected(set)asymmetric visibility on$systemand$middlewareStack.- Constructor property promotion on
$logger. - Typed-constant defaults (
Constant::ENV_PROD). #[\Override]on every method overridingKernelInterface.
๐งญ 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.
As the framework assembly package, waffle lives under the top-level Waffle namespace (not Waffle\Commons\*). Production code under Waffle may depend only on:
Waffle\**โ itself (the kernel, handlers, events, and factories)Waffle\Commons\Contracts\**โ the shared contracts packageWaffle\Commons\Utils\**โ theClassParserdiscovery helperPsr\**โ PSR interfaces (PSR-7 / PSR-11 / PSR-15 / PSR-17)@global+Psl\**โ PHP core and the PHP Standard Library
Test code under WaffleTests 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.
Note: waffle depends only on contracts + utils directly. The concrete components (http, routing, security, โฆ) are wired at the application layer (e.g. the skeleton's AppKernelFactory), not pulled in here โ that is what keeps the kernel component-agnostic.
๐งช Testing
docker exec -w /waffle-commons/waffle waffle-dev composer tests
๐ License
MIT โ see LICENSE.md.