Architecture · PSR-15 pipeline

How a request moves through a Dotkernel application

Every Dotkernel application is a stack of PSR-15 middleware with no framework runtime hidden underneath it. A request enters at the error boundary, passes through routing, negotiation, authentication and authorization, and reaches your handler - and you can read every layer it crossed on the way.

Contract PSR-7 + PSR-15
Container PSR-11
Logging PSR-3
Five phases19 stages
Error boundary1 stage
Request preparation4 stages
Routing4 stages
Contract & headers4 stages
Identity2 stages
Dispatch & fallback4 stages
Nineteen stages, folded
The premise

Framework-less, not framework-free

Dotkernel is an opinionated, framework-less tool aimed at intermediate-to-advanced programmers. Framework-less does not mean nothing is underneath: Mezzio and Laminas components are there, and they are excellent. It means there is no framework runtime deciding things on your behalf. The pipeline is a list you wrote, in a file you can open, executed top to bottom.

That is the whole trade. You give up the convenience of conventions you never have to look at, and you get an application where every layer between the socket and your handler is named, ordered and yours to reorder.

Extending the power of

Gradual learning curve

Fine-tuned security

KISS, YAGNI, DRY

Long-term support

Middleware flow

Nineteen stages, in the order they run

This is the default pipeline of Dotkernel API, unedited. Each stage receives the request, may hand it onward, and may alter the response on the way back out - which is why the error boundary is first in and last out.

Mezzio / Laminas component Dotkernel
Error boundary
01
ProblemDetailsMiddleware mezzio/mezzio-problem-details

The outermost layer, deliberately. Anything thrown deeper in the stack is handed to dot-errorhandler for logging and comes back as a problem details response, so a failure reaches the client as a documented error shape rather than a stack trace.

Request preparation
02
MalformedRequestBodyMiddleware dotkernel/api

Rejects a body that is not valid JSON up front, before any later stage tries to read it and fails in a less legible place.

03
BodyParamsMiddleware mezzio/mezzio-helpers

Parses the raw request body into the parsed body according to its Content-Type.

04
ServerUrlMiddleware mezzio/mezzio-helpers

Seeds the server URL helper with the current scheme and host, so anything generating a URL later produces an absolute one.

05
CorsMiddleware mezzio/mezzio-cors

Recognises preflight requests and answers them, and attaches the configured CORS headers to everything else. It sits before routing so a preflight never needs a matched route.

Routing
06
RouteMiddleware mezzio/mezzio-router Route

Matches the request against the route table and registers the RouteResult attribute. Everything below this line can ask which route was matched - and most of it does.

07
ImplicitHeadMiddleware mezzio/mezzio-router

Answers HEAD for routes that only declare GET.

08
ImplicitOptionsMiddleware mezzio/mezzio-router

Answers OPTIONS with the methods the matched path actually allows.

09
MethodNotAllowedMiddleware mezzio/mezzio-router

Returns 405 Method Not Allowed when the path matched but the method did not. Order matters here: it has to sit after both implicit handlers, or it would answer the requests they exist to serve.

Contract & headers
10
ContentNegotiationMiddleware dotkernel/api

Reconciles the client's Accept and Content-Type with what the matched route is configured to serve, so diverse consumers share one API without custom glue.

11
DeprecationMiddleware dotkernel/api

The mechanism behind API evolution. It reads the #[ResourceDeprecation] attribute off the handler and, when one is present, sets the sunset and link response headers - the client learns an endpoint is going away from the endpoint itself, rather than from a migration guide.

12
ResponseHeaderMiddleware dotkernel/dot-response-header

Applies the headers you configure application-wide to every outgoing response, in one place instead of per handler.

13
UrlHelperMiddleware mezzio/mezzio-helpers

Seeds the URL helper with the routing result, so handlers and HAL links can be generated relative to the route that was actually matched.

Identity
14
AuthenticationMiddleware dotkernel/api 401

Validates the OAuth 2.0 Bearer token. Routes declared as authenticated answer 401 Unauthorized without a valid one, and never reach the handler.

15
AuthorizationMiddleware dotkernel/api 403

Checks the authenticated identity's role against the permissions allocated to the matched route name, and returns 403 Forbidden when it falls short. The decision lives in configuration, not scattered through handlers.

Dispatch & fallback
16
ResourceProviderMiddleware dotkernel/api

Reads the #[Resource] attribute on the handler's handle() method and loads the named entity through Doctrine using the route placeholder. The handler receives a resource rather than an identifier, and a record that does not exist becomes a 404 before your code runs.

17
DispatchMiddleware mezzio/mezzio-router

Hands the request to the matched PSR-15 handler. This is your code - everything above was getting the request into a state where your handler only has to do its own job.

18
ProblemDetailsNotFoundHandler mezzio/mezzio-problem-details

Reached only when nothing above returned a response: emits 404 in problem details form.

19
GetNotFoundResourceHandler dotkernel/api

The last stage in the pipeline, shaping the not-found response for API clients.

Library flow

The packages underneath, in the same order

The same journey seen one level down - which library owns each step. The second column shows how little changes when a feature is added: sending an email inserts one package and leaves the rest of the flow alone.

Default flow

A request that reads or writes data and returns a resource.

Request
mezzio/mezzio-problem-details
dotkernel/dot-errorhandler
mezzio/mezzio-cors
mezzio/mezzio-router
dotkernel/dot-response-header
mezzio/mezzio-helpers
mezzio/mezzio-authentication
mezzio/mezzio-authorization
mezzio/mezzio-router dispatch
laminas/laminas-diactoros not found
dotkernel/dot-dependency-injection
doctrine/orm
mezzio/mezzio-hal
laminas/laminas-diactoros
psr/http-message
Response

Flow for sending an email

Identical until the domain layer, where dot-mail joins in.

Request
mezzio/mezzio-problem-details
dotkernel/dot-errorhandler
mezzio/mezzio-cors
mezzio/mezzio-router
dotkernel/dot-response-header
mezzio/mezzio-helpers
mezzio/mezzio-authentication
mezzio/mezzio-authorization
mezzio/mezzio-router dispatch
laminas/laminas-diactoros not found
dotkernel/dot-dependency-injection
doctrine/orm
dotkernel/dot-mail added
mezzio/mezzio-hal
laminas/laminas-diactoros
psr/http-message
Response
Standards

Built on interfaces from the Framework Interop Group

Dotkernel applications are collections of PSR-7 middleware. The standards below are not badges - they are the reason a package from one vendor drops into a pipeline built by another.

PSR-7

HTTP messages are immutable value objects. Every stage above receives a request and returns a response, and neither is a global you have to guard.

PSR-15

Middleware and request handlers share one interface, which is what makes the pipeline a plain ordered list rather than a framework-specific event graph.

PSR-11

The application is container-based, with each module declaring its dependencies in a ConfigProvider.

PSR-4

Classes are located by autoloader from their namespace, so where a file lives is never a question of configuration.

PSR-3

Errors are logged through LoggerInterface via dot-errorhandler, reached from the error boundary at stage one.

Why it matters

You can delete any stage in the pipeline, or write your own and pipe it in, without asking a framework's permission. That is the practical payoff of standardising on interfaces.

Across the platform

One pipeline shape, three applications

The stages differ where the job differs - Admin adds sessions and CSRF-protected forms, Queue swaps HTTP dispatch for a message consumer - but the pattern is constant, and all three declare the same Core namespaces underneath.

Shared domain layer Core\App Core\Admin Core\User Core\Security Core\Setting

Kept as its own Git repository and added as a submodule, so an entity means the same thing to the endpoint that creates it, the admin screen that moderates it and the worker that emails it.

API

The pipeline documented above, in full.

Admin

Same shape, plus sessions, CSRF-protected forms and 2FA in the identity phase.

Queue

The same middleware thinking applied to messages instead of requests.

Read it yourself

The pipeline is one file

Everything on this page comes from config/pipeline.php - about ninety lines, most of them comments explaining where to add your own middleware. There is no generated layer between that file and what runs.

php ./bin/cli.php route:list

Once the pipeline has routed a request, the route table is the other half of the picture. This command lists every endpoint the application exposes, which is the fastest way to review an API you have just inherited.

Database migrations are generated and run the same way, through the Doctrine console commands registered by dot-cli.

Open source, in production

No layer you are not allowed to read.

Dotkernel is developed and led by the dev team at Apidemia - built first as an internal tool for handling complex architectures, released under MIT as our way of giving back to the community.