Guide

Middleware

Use middleware for shared HTTP concerns, attach it to routes or groups, and keep business logic out of it.

What middleware is for

In Lemonade, middleware is a PSR-15 layer around HTTP request processing. It fits request conditions, headers, CSRF, or access checks before the controller. Keep the boundary clear: middleware = shared HTTP behavior, controller = a concrete HTTP use case and orchestration, service = application or business operations. For a complete CSRF form example, continue to Forms & POST Workflow.

Attach middleware to a route or group

Use route middleware when one endpoint needs an extra HTTP rule. Use group middleware when several routes share the same concern. In both cases you register classes by name and the runtime resolves them from the container before the controller.

example.php
<?php

use App\Controllers\ArticleController;
use App\Controllers\ProfileController;
use App\Http\Middleware\RequirePreviewHeaderMiddleware;
use App\Http\Middleware\RequireProfileHeaderMiddleware;
use Lemonade\Framework\Routing\Router;

return static function (Router $router): void {
    $router
        ->getNamed('articles.preview', '/articles/preview', ArticleController::class . '@preview')
        ->middleware(RequirePreviewHeaderMiddleware::class);

    $router
        ->group('/profile', static function (Router $router): void {
            $router->getNamed('profile.show', '', ProfileController::class . '@show');
            $router->postNamed('profile.update', '/edit', ProfileController::class . '@update');
        })
        ->middleware(RequireProfileHeaderMiddleware::class);
};

Ordering and short-circuiting

Middleware runs in registration order. Order matters because one layer can modify the request, add response headers, or stop the pipeline early. return $handler->handle($request); passes the request on. Returning your own response ends that branch without running the rest of the pipeline.

example.php
<?php

use App\Http\Middleware\RequestIdMiddleware;
use Lemonade\Framework\Core\Context\ApplicationContextFactory;
use Lemonade\Framework\Core\KernelFactory;
use Lemonade\Framework\Http\Middleware\ErrorHandlingMiddleware;
use Lemonade\Framework\Http\Middleware\MiddlewareStack;

$context = (new ApplicationContextFactory())->fromGlobals(__DIR__);
$kernel = (new KernelFactory())->create($context);

$kernel->framework()->middleware(static function (MiddlewareStack $stack): void {
    $stack->insertAfter(ErrorHandlingMiddleware::class, RequestIdMiddleware::class);
});

$kernel->handle();

Write a small custom middleware

Custom middleware implements MiddlewareInterface and receives dependencies through constructor injection. Keep it small and focused on HTTP. Once it starts handling persistence or reusable business logic, move that work into an application service.

example.php
<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class RequirePreviewHeaderMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly ResponseFactoryInterface $responses,
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if ($request->getHeaderLine('X-Preview-Mode') !== 'allow') {
            return $this->responses->createResponse(403);
        }

        return $handler->handle($request);
    }
}