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-beta7
- waffle-commons/utils: 0.1.0-beta7
Requires (Dev)
- carthage-software/mago: ^1.59
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.10.1
- php-mock/php-mock-phpunit: ^2.16
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.20
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 02:00:33 UTC
README
Waffle β the Kernel
Release:
0.1.0-beta7Β |Β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-7 β the serializer seam (API-02)
When the application registers a Contracts\Serializer\DtoSerializerInterface (waffle-commons/serializer), the kernel uses it at the HTTP boundary:
ControllerArgumentResolverhydrates a#[Dto]parameter through it: from the parsed body when the request factory parsed one, otherwise from the raw body, decoded within the serializer's limits. PUT and PATCH bodies, which the request factory leaves unparsed, now hydrate DTOs too.ControllerResponseConverterwrites a returned#[Dto]as a200JSON response, the shape the OpenAPI document describes.ControllerDispatchertakes the serializer by constructor and hands it to its default converter;AbstractKernel's default wiring passes the registered serializer to the resolver and the dispatcher.
Without a serializer, every path behaves as before. The kernel also exposes its container as the readable $container property that Contracts\Core\KernelInterface now declares.
π 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
π Documentation
Central framework docs (DiΓ‘taxis) for this component:
- Reference:
reference/core.md,reference/aot.md - Explanation:
explanation/architecture.md,explanation/lifecycle.md - Full documentation tree: waffle-commons/documentation
π License
MIT β see LICENSE.md.