nih / http-kernel
Minimal HTTP kernel for PSR-7, PSR-11, PSR-15 and PSR-17 based applications.
Requires
- php: 8.4 - 8.5
- httpsoft/http-message: ^1.1
- nih/container: ^0.1.3
- nih/middleware-dispatcher: ^0.2.0
- nih/router: ^0.2.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.5
README
Minimal HTTP kernel for applications built around PSR-7, PSR-15 and PSR-17.
nih/http-kernel is for applications that want a thin, explicit HTTP runtime without adopting a full microframework. It uses NIH container/router packages by default and gives you ordered bootstraps, a main application pipeline, a separate error pipeline, and a safe deferred handoff model for post-response or legacy execution.
If you want a ready-to-run minimal application with conventional project layout around this kernel, start with nih/app-skeleton. The examples below focus on the kernel-facing pieces.
Why use it
- small application surface:
services,routes,pipeline,errorPipeline,fatal - boot-time configuration stays separate from request-time execution
errorPipelineis independent from the normal application pipeline- optional PSR-3 error logging through
ErrorLoggingMiddleware - built on focused packages:
nih/container,nih/router,nih/middleware-dispatcher - supports post-response and legacy handoff through
DeferredCallableResponse
When to choose it
Use this package when:
- you want your own PSR-based application skeleton instead of a full framework
- you need explicit control over bootstrap order, middleware composition, routing, and error rendering
- you want gradual migration from legacy code
This package is probably not the best fit when:
- you want a batteries-included microframework with a larger built-in ecosystem
- you prefer framework conventions over assembling your own application structure
Requirements
- PHP
8.4 - 8.5
Installation
composer require nih/http-kernel
If you want a ready-to-use starter project with a complete minimal application, see nih/app-skeleton.
If you need a legacy fallback when routing returns NOT_FOUND, see nih/legacy-gateway.
Quick start
This example shows the smallest routed application. GET / dispatches HomeAction, and unmatched routes fall through to the default NotFoundHandler from RoutingBootstrap.
public/index.php
<?php declare(strict_types=1); use App\Bootstrap\AppBootstrap; use NIH\Container\ContainerConfig; use NIH\HttpKernel\Bootstrap\ErrorHandlingBootstrap; use NIH\HttpKernel\Bootstrap\Psr17Bootstrap; use NIH\HttpKernel\Bootstrap\RoutingBootstrap; use NIH\HttpKernel\HttpRunner; require dirname(__DIR__) . '/vendor/autoload.php'; $deferred = (new HttpRunner(new ContainerConfig(shared: true))) ->boot([ Psr17Bootstrap::class, ErrorHandlingBootstrap::class, RoutingBootstrap::class, AppBootstrap::class, ]) ->run(); if ($deferred !== null) { $deferred(); }
boot() accepts an ordered list of bootstrap class names implementing BootstrapInterface.
src/Bootstrap/AppBootstrap.php
<?php declare(strict_types=1); namespace App\Bootstrap; use App\Action\HomeAction; use NIH\HttpKernel\Bootstrap\BootstrapInterface; use NIH\HttpKernel\HttpApplication; use NIH\Router\Middleware\RouteMatchMiddleware; final class AppBootstrap implements BootstrapInterface { public static function boot(HttpApplication $app): void { $app->routes->path('/') ->action('', HomeAction::class, '__invoke', ['GET']); $app->services->auto(RouteMatchMiddleware::class) ->argument('useRequestSite', false); } }
src/Action/HomeAction.php
<?php declare(strict_types=1); namespace App\Action; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamFactoryInterface; final readonly class HomeAction { public function __construct( private ResponseFactoryInterface $responseFactory, private StreamFactoryInterface $streamFactory, ) { } public function __invoke(ServerRequestInterface $request): ResponseInterface { $response = $this->responseFactory->createResponse() ->withHeader('Content-Type', 'text/plain; charset=utf-8'); return $response->withBody( $this->streamFactory->createStream('hello from nih/http-kernel'), ); } }
RoutingBootstrap appends RouteMatchMiddleware and RouteDispatchMiddleware, and keeps NotFoundHandler as the fallback final handler.
The RouteMatchMiddleware override keeps this starter host-agnostic by matching only the request path. If your application uses site() trees, remove that override and configure sites explicitly.
With the default NIH container, class-name action targets and their constructor dependencies are resolved via autowiring by default.
For a routed example and more customization recipes, see docs/recipes.md.
Core package APIs
nih/http-kernel stays intentionally small and delegates most configuration APIs to three focused packages:
nih/container:ContainerConfig, service definitions, autowiring, aliases, shared vs non-shared servicesnih/router:RouterConfig, route trees, route matching, URL generation, routing middlewaresnih/middleware-dispatcher:Pipeline, middleware ordering, final handlers, request-time dispatch
Important constraints
- bootstraps must only configure
HttpApplication; they must not start output buffering, emit responses, mutate globals, or install PHP handlers - the front controller must execute the closure returned by
HttpRunner::run()when the response is deferred - application code may temporarily mutate the PHP error-handler and exception-handler stacks during request handling; before
HttpRunner::run()returns, the runner unwinds them back to the handlers that were active before the run
Mental model
The package is built around two classes:
NIH\HttpKernel\HttpApplication: mutable boot-time configurationNIH\HttpKernel\HttpRunner: runtime orchestrator
HttpApplication intentionally exposes only:
servicesvianih/containerroutesvianih/routerpipelinevianih/middleware-dispatchererrorPipelinevianih/middleware-dispatcherfatal
Typical lifecycle:
- bootstrap classes configure
HttpApplication HttpRunnercreates the request from globals and runs the mainpipeline- thrown exceptions are routed through
errorPipeline - deferred responses return a closure to the caller
- fatal shutdown handling remains a separate fallback path
Default bootstraps
Order matters. The package currently provides:
Psr17BootstrapErrorHandlingBootstrapRoutingBootstrap
Psr17Bootstrap
Registers default PSR-17 HTTP message factories.
ErrorHandlingBootstrap
Configures the default errorPipeline:
- appends
ErrorFormatMiddleware - sets
PlainTextErrorHandleras the final error handler - wires default handlers for:
application/jsonapplication/xmltext/xmltext/htmltext/plain
Negotiated JSON/XML/HTML handlers are used only when the client sends a specific Accept header. An empty Accept header or Accept: */* falls through to the final PlainTextErrorHandler.
RoutingBootstrap
Configures router services and the default main pipeline:
RouteMatchMiddlewareRouteDispatchMiddlewareNotFoundHandleras final handler
RouteMatcher and UrlGenerator are wired against HttpApplication->routes.
Related packages
nih/app-skeleton: minimal starter project withconfig/app.php, a thinpublic/index.php, and app-specific bootstrapsnih/legacy-gateway: optional legacy handoff module that falls through to a legacy entrypoint only when routing returnedNOT_FOUND
Further documentation
- docs/recipes.md: routed example, error customization, deferred callbacks, fatal override, legacy handoff
- docs/runtime.md: request creation, error-pipeline contract, deferred handoff, fatal shutdown behavior
Development
composer installcomposer testcomposer validate --strict --no-check-lock
Unit tests live under tests/Unit/. Runtime and fatal subprocess coverage lives under tests/E2E/.