Search by

A modern, minimalist, and security-focused PHP micro-framework.

Package info

github.com/waffle-commons/waffle

pkg:composer/waffle-commons/waffle

Statistics

Installs: 128

Dependents: 2

Suggesters: 0

Stars: 3

Open Issues: 1

0.1.0-beta6 2026-09-08 19:56 UTC

README

Discord PHP Version Require PHP CI codecov Latest Stable Version Latest Unstable Version Total Downloads Packagist License

Waffle β€” the Kernel

Release: 0.1.0-beta6 Β |Β  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 under Psr\Http\Server\RequestHandlerInterface β€” there is no hard-coded new ControllerDispatcher(...) on the hot path. configure() registers a default ControllerDispatcher only 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. ControllerArgumentResolver hydrates #[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 unified ValidationException, which the error handler renders as RFC 7807 422 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

  1. handle(ServerRequestInterface) ensures boot() + configure() have run (idempotent β€” the $booted guard short-circuits subsequent calls).
  2. validateState() rejects an unconfigured kernel with RuntimeException / ContainerException / NotFoundException as appropriate.
  3. RequestReceivedEvent is dispatched; listeners may swap the request.
  4. The middleware stack runs; the terminal handler is a ControllerDispatcher resolving the route's _controller + _route_params.
  5. ResponseGeneratedEvent is dispatched; listeners may swap the response.
  6. The response is returned to WaffleRuntime which emits it and calls terminate() for post-emission listeners.
  7. 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 $system and $middlewareStack.
  • Constructor property promotion on $logger.
  • Typed-constant defaults (Constant::ENV_PROD).
  • #[\Override] on every method overriding KernelInterface.

🧭 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 package
  • Waffle\Commons\Utils\** β€” the ClassParser discovery helper
  • Psr\** β€” 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:

πŸ“„ License

MIT β€” see LICENSE.md.