Request Lifecycle
An HTTP request flows from the web server through public/index.php, service provider registration,
resolution of Router and PSR-7 Request, League Route dispatch, and response emission
via SapiEmitter.
Prerequisites: setup entry points, configuration.
Overview
flowchart TB
WS["Web server"]
IDX["public/index.php"]
BOOT["bootstrap/app.php"]
PROV["registerServiceProviders()"]
RUN["App::run()"]
ROUTER["Router::dispatch(Request)"]
MW["Middleware stack"]
STRAT["RouteStrategy"]
INT["Route interceptors"]
RES["Autowire: FormRequest, params, Request"]
CTRL["Controller / closure"]
EMIT["SapiEmitter"]
WS --> IDX --> BOOT --> PROV
IDX --> RUN --> ROUTER --> MW --> STRAT
STRAT --> INT --> RES --> CTRL --> EMIT
Web server → public/index.php
→ vendor/autoload.php
→ bootstrap/app.php
→ App::create() + registerServiceProviders()
→ for each provider: register(), then boot()
→ ComponentsServiceProvider::boot() loads component routes
→ App::run()
→ container->get(RouterInterface)
→ container->get(ServerRequestInterface)
→ Router::dispatch(Request)
→ middleware (global + route)
→ RouteStrategy::invokeRouteCallable()
→ route interceptors
→ autowire handler arguments
→ PSR-7 Response
→ SapiEmitter → client
1. Entry point
public/index.php is the only file the document root should point to.
It contains no business logic:
require_once __DIR__ . '/../vendor/autoload.php';
/** @var \Concept\Core\App $app */
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->run();
2. Bootstrap (before run)
bootstrap/app.php performs:
App::create($rootPath, $paths)— League Container,ReflectionContainer,PathManager, early Whoops (readsAPP_DEBUGfrom$_ENV).registerServiceProviders()— loads the list frombootstrap/providers/app.php.
League Container calls register() for each provider, then immediately
boot() (if BootableServiceProviderInterface is implemented) — see
Service providers → register and boot.
Order in providers/app.php is critical: provider N in boot() can
use services only from providers higher in the list.
Provider order (HTTP, skeleton)
Actual order in bootstrap/providers/app.php:
- ConfigServiceProvider — config,
.env, timezone - LocaleServiceProvider — locale for validation translations
- TelemetryServiceProvider — telemetry collection (if enabled)
- ErrorHandlerServiceProvider — Whoops (HTML / JSON / production fallback)
- HttpServiceProvider — PSR-7 request, Router, response factories
- SessionServiceProvider — Symfony Session (starts on first access)
- DataMaskerServiceProvider — masking sensitive data in logs
- LogServiceProvider — Monolog
- ViewRegistryServiceProvider, TwigServiceProvider
- ValidationServiceProvider, DatabaseServiceProvider, CastingServiceProvider
- DebugLoggerServiceProvider — in-memory debug log (dev)
- ComponentsServiceProvider — modules (last in the list)
What ComponentsServiceProvider::boot() does
ComponentsServiceProvider is last so that in boot() it already has
ConfigInterface, RouterInterface, view registries, and so on.
During boot() (still before App::run()), if components are enabled:
- component CLI commands, migrations, and seeders are registered;
- additional component service providers are loaded (
addServiceProvider()); requireof each component’sroutes.php— routes are added to the sameRouter;- Twig paths, extensions, and contexts for component views are registered (HTTP only, not CLI).
Global routes from routes/web.php are loaded on first resolution of
RouterInterface in the HttpServiceProvider factory — usually during
ComponentsServiceProvider::boot(), when it requests the Router for component routes.
3. PSR-7 request
ServerRequestInterface is created in the container via
Laminas\Diactoros\ServerRequestFactory::fromGlobals() —
data from $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES.
In a typical HTTP cycle, resolution happens in App::run() before dispatch().
After a route match, RouteStrategy adds URL parameters to request attributes
({id} → $request->getAttribute('id')) and updates the shared instance
in the container.
4. App::run()
/** @var RouterInterface $router */
$router = $this->container->get(RouterInterface::class);
/** @var ServerRequestInterface $request */
$request = $this->container->get(ServerRequestInterface::class);
$response = $router->dispatch($request);
(new SapiEmitter)->emit($response);
Router (class Concept\Core\Http\Routing\Router) — wrapper around League Route
with RouteStrategy: DI for handler parameters, FormRequest, route interceptors, telemetry.
5. Middleware
Middleware is registered in route files via
$router->lazyMiddleware(ClassName::class) (League Route).
Classes are created from the container during request handling.
Each middleware implements MiddlewareInterface::process($request, $handler)
and calls $handler->handle($request) to pass control down the chain.
Core includes, among others:
HandleValidationExceptionMiddleware— convertsValidationExceptionto a responseVerifyCsrfTokenMiddleware— CSRF for unsafe methodsStorePreviousUrlMiddleware— stores URL for redirectsShareViewDataMiddleware— shared variables for Twig (csrf_token, etc.)ParseJsonBodyMiddleware,ForceJsonResponseMiddleware— JSON API
Minimal skeleton does not attach middleware globally — only the / route.
More details — Middleware.
6. RouteStrategy: interceptors and handler
After middleware, League Route calls RouteStrategy::invokeRouteCallable():
- Route interceptors — classes from
config/routes.php→routes.interceptors(RouteInterceptorInterface::intercept()); for ACL, policies, etc. — see Route interceptors. - URL parameters in Request —
{id}becomes a request attribute. - Autowire handler arguments — for a controller method or closure:
FormRequest— from the container, validation; on failure —ValidationExceptionServerRequestInterface— current request- route parameters — type-cast via
CasterInterface(Valinor) - default values in the signature
Route example:
$router->get('/', [IndexController::class, 'index'])->setName('home');
Controller [Class, 'method'] is resolved from the container — constructor dependencies
(ViewResponseFactoryInterface, etc.) are injected via League Container +
ReflectionContainer.
More details — Controllers → autowiring,
method arguments.
If telemetry.enabled, RouteStrategy records
TelemetryEvent::HTTP_ROUTE_INTERCEPTOR_EXECUTED (per interceptor) and
TelemetryEvent::HTTP_ROUTE_CALLABLE_INVOKED (handler execution time).
7. Response
The handler returns ResponseInterface (PSR-7). Typical options:
ViewResponseFactoryInterface::create('@frontend/index')— HTML via TwigResponseFactoryInterface— JSON, redirect, empty response
SapiEmitter (Laminas HttpHandlerRunner) sends headers and body to the PHP SAPI
(Apache, FPM, built-in server). More details — Responses.
8. Error handling
- Before providers — early Whoops in
App::create()(readsAPP_DEBUGfrom$_ENV). - ErrorHandlerServiceProvider::boot() — switches Whoops: PrettyPage (debug), PlainText (CLI), JSON (
RequestFormat::expectsJson()), production fallback fromPathName::ERRORS_FALLBACK_VIEWS. - Validation —
ValidationExceptionfrom FormRequest;HandleValidationExceptionMiddlewaremay return 422 / redirect. - CSRF —
CsrfExceptionon invalid token. - 404 / method — League Route; response depends on router settings and middleware.
9. Telemetry (optional)
If telemetry.enabled = true, the core records bootstrap and HTTP stages via
TelemetryCollector: service awakening (FRAMEWORK_SERVICE_AWAKENING),
component registration, route file loading (FRAMEWORK_ROUTES_REGISTERED),
interceptor and handler spans, full request summary (HTTP_REQUEST_HANDLED in App::run()).
Optional: SQL (telemetry.db_queries) and log mirroring (telemetry.logs).
CLI — separate cycle
bin/console.php does not call App::run().
It uses the same paths.php and App::create(), but providers from
bootstrap/providers/console.php and Symfony Console Application::run().
HTTP middleware, PSR-7 request, and Router::dispatch() do not apply in CLI.
Next steps
- Routing — declaring routes, names, groups
- Route interceptors — ACL, exceptions, redirect via middleware
- Middleware — custom middleware
- Controllers
- Requests and validation — FormRequest