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:

  1. App::create($rootPath, $paths) — League Container, ReflectionContainer, PathManager, early Whoops (reads APP_DEBUG from $_ENV).
  2. registerServiceProviders() — loads the list from bootstrap/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:

  1. ConfigServiceProvider — config, .env, timezone
  2. LocaleServiceProvider — locale for validation translations
  3. TelemetryServiceProvider — telemetry collection (if enabled)
  4. ErrorHandlerServiceProvider — Whoops (HTML / JSON / production fallback)
  5. HttpServiceProvider — PSR-7 request, Router, response factories
  6. SessionServiceProvider — Symfony Session (starts on first access)
  7. DataMaskerServiceProvider — masking sensitive data in logs
  8. LogServiceProvider — Monolog
  9. ViewRegistryServiceProvider, TwigServiceProvider
  10. ValidationServiceProvider, DatabaseServiceProvider, CastingServiceProvider
  11. DebugLoggerServiceProvider — in-memory debug log (dev)
  12. 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());
  • require of each component’s routes.php — routes are added to the same Router;
  • 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 — converts ValidationException to a response
  • VerifyCsrfTokenMiddleware — CSRF for unsafe methods
  • StorePreviousUrlMiddleware — stores URL for redirects
  • ShareViewDataMiddleware — 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():

  1. Route interceptors — classes from config/routes.phproutes.interceptors (RouteInterceptorInterface::intercept()); for ACL, policies, etc. — see Route interceptors.
  2. URL parameters in Request{id} becomes a request attribute.
  3. Autowire handler arguments — for a controller method or closure:
    • FormRequest — from the container, validation; on failure — ValidationException
    • ServerRequestInterface — 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 Twig
  • ResponseFactoryInterface — 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() (reads APP_DEBUG from $_ENV).
  • ErrorHandlerServiceProvider::boot() — switches Whoops: PrettyPage (debug), PlainText (CLI), JSON (RequestFormat::expectsJson()), production fallback from PathName::ERRORS_FALLBACK_VIEWS.
  • ValidationValidationException from FormRequest; HandleValidationExceptionMiddleware may return 422 / redirect.
  • CSRFCsrfException on 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.