meritum/http#
meritum/http extends georgeff/kernel with routing, middleware, and request handling. Everything in the kernel guide
applies here — modules, service definitions, tagging, decoration, and the boot lifecycle all work identically.
composer require meritum/httpEntry Point#
HttpKernel is the concrete implementation. Wire it up in your entry point script:
use App\ModuleRepository;
use Meritum\Http\HttpKernel;
use Georgeff\Kernel\Support\Env;
use Georgeff\Kernel\Environment;
$environment = match (Env::get('APP_ENV', 'production')) {
'local' => Environment::Local,
'test', 'testing' => Environment::Testing,
'dev', 'develop', 'development' => Environment::Development,
'stage', 'staging' => Environment::Staging,
default => Environment::Production,
};
$kernel = new HttpKernel($environment, debug: Env::get('APP_DEBUG', false));
$kernel->addRepository(new ModuleRepository());
$kernel->boot();
$kernel->run();run() resolves the incoming request from globals, passes it through the middleware stack and router, emits the response, runs terminating callbacks via terminate(), then shuts the kernel down.
Routing#
Register routes before boot(), either directly on the kernel or inside a module:
$kernel->addRoute('GET', '/', Handler\HomeHandler::class);
$kernel->addRoute(['GET', 'HEAD'], '/health', Handler\HealthHandler::class);
$kernel->addRoute('POST', '/users', Handler\CreateUserHandler::class);The handler can be a class string (resolved from the container at request time) or a RequestHandlerInterface instance.
Handlers are PSR-15 RequestHandlerInterface implementations:
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class HomeHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new JsonResponse(['status' => 'ok']);
}
}Route Parameters#
Capture URI segments using {name} placeholders:
$kernel->addRoute('GET', '/users/{id}', Handler\ShowUserHandler::class);Parameters are available via the route attached to the request attributes:
use Meritum\Http\Routing\RouteInterface;
final class ShowUserHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
/** @var RouteInterface $route */
$route = $request->getAttribute(RouteInterface::class);
$id = $route->getArgument('id');
return new JsonResponse(['id' => $id]);
}
}The route is also available under the '__route__' string key as a convenience alias for code that does not import RouteInterface.
Registering Routes from a Module#
Route registration inside a module requires narrowing KernelInterface to HttpKernelInterface:
use Georgeff\Kernel\KernelInterface;
use Meritum\Http\HttpKernelInterface;
use Georgeff\Kernel\Module\ModuleInterface;
final class AppModule implements ModuleInterface
{
public function register(KernelInterface $kernel): void
{
assert($kernel instanceof HttpKernelInterface);
$kernel->define(Handler\HomeHandler::class, fn() => new Handler\HomeHandler());
$kernel->addRoute('GET', '/', Handler\HomeHandler::class);
}
}Middleware#
Global Middleware#
Add PSR-15 middleware to the global stack with addMiddleware(). Middleware runs in registration order, wrapping every request:
$kernel->addMiddleware(Middleware\CorsMiddleware::class);
$kernel->addMiddleware(Middleware\AuthMiddleware::class);As with handlers, a class string is resolved from the container at request time.
Per-Route Middleware#
Attach middleware to a specific route via the RouteInterface returned by addRoute():
$kernel->addRoute('DELETE', '/users/{id}', Handler\DeleteUserHandler::class)
->addMiddleware(Middleware\RequireAdminMiddleware::class);Per-route middleware runs after global middleware, only for matching requests.
Exception Handling#
By default, exceptions that escape the middleware stack are re-thrown. To handle them and convert them to responses, register an implementation of ExceptionHandlerInterface in the container:
use Throwable;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Meritum\Http\Exception\ExceptionHandlerInterface;
use Meritum\Http\Exception\HttpExceptionInterface;
final class ExceptionHandler implements ExceptionHandlerInterface
{
public function handle(Throwable $e, ServerRequestInterface $request): ResponseInterface
{
if ($e instanceof HttpExceptionInterface) {
return new JsonResponse(
['error' => $e->getTitle()],
$e->getStatusCode()
);
}
return new JsonResponse(['error' => 'Internal Server Error'], 500);
}
}Register it in a module:
use Meritum\Http\Exception\ExceptionHandlerInterface;
$kernel->define(ExceptionHandlerInterface::class, fn() => new ExceptionHandler());The kernel checks for ExceptionHandlerInterface::class in the container on every request. If present, caught exceptions are passed to it. If absent, exceptions bubble.
meritum/http-exception-handlerprovides a production-ready implementation that integrates with themeritum/structured-loggingpipeline. See the http-exception-handler guide for details.
HTTP Exceptions#
meritum/http ships three HTTP exception classes for common failure cases:
use Meritum\Http\Exception\NotFoundHttpException;
use Meritum\Http\Exception\MethodNotAllowedHttpException;
use Meritum\Http\Exception\HttpException;
throw new NotFoundHttpException($request);
throw new MethodNotAllowedHttpException($request);
throw new HttpException($request, 'Custom message'); // 500 by default
All three implement HttpExceptionInterface, which your exception handler can check to extract the status code and title.
Terminating Callbacks#
Register callbacks to run after the response has been emitted:
$kernel->onTerminating(function (ServerRequestInterface $request, ResponseInterface $response, KernelInterface $kernel): void {
// Runs after the response is sent to the client
});Multiple callbacks run in registration order. terminate() does not shut the kernel down — shutdown() is called by run() after all terminating callbacks have completed.
When calling handle() and terminate() directly (e.g. in tests), shutdown is your responsibility:
$kernel->boot();
$response = $kernel->handle($request);
$kernel->terminate($request, $response);
$kernel->shutdown();Default Container Registrations#
HttpKernel registers two services automatically during the preBoot phase:
| ID | Resolves to |
|---|---|
RequestHandlerInterface::class |
The router (wrapped middleware stack + route dispatch) |
ServerRequestInterface::class |
ServerRequestFactory::fromGlobals() |
Both are shared. Do not overwrite these unless you intend to replace the router or request resolution entirely.