Request lifecycle

Request lifecycle

What happens from the moment the browser knocks on the site to the moment HTML is returned.

Step by step

  1. Web server. nginx sits in front and serves static files (css/js/img) directly. PHP requests are proxied to Apache/PHP-FPM, which — via .htaccess — routes everything to index.php (front) or admin.php (admin).
  1. Entry point. index.php checks for install.lock (otherwise it redirects to the installer) and includes core/bootstrap.php.
  1. Bootstrap + Kernel. core/bootstrap.php creates the Kernel, which:
    • loads the Autoloader (PSR-4);
    • builds the DI Container;
    • reads Config from .env and config/*.php;
    • registers services: Connection (DB, lazy), Engine (templates), Router, Logger, Locale, Translator, Auth, Options and others;
    • starts the session (unless CLI).
  1. Locale. The active language is detected (Locale::detect(): ?lang → cookie → Accept-Language → default) and set on the Translator.
  1. Routes. bootstrap.php registers public and admin routes. Finally the active plugins are loaded (PluginManager::load()) — their plugin.php files add their own routes, hooks and tags.
  1. Dispatch. Kernel::handle($request) calls Router::dispatch(). The router matches the path against route patterns and finds the handler.
  1. Middleware. If the route has middleware (e.g. AuthMiddleware for /admin/*), the request passes through them.
  1. Controller. The router resolves the controller from the container (with dependency auto-injection) and calls the (Request $request, array $params) method. The controller returns a Response.
  1. Response. Kernel::send($response) sets the status, headers and outputs the body.

The router

A route is registered like this:

$router->get('/news/{slug:[^/]+}', NewsController::class . '@publicShow');
$router->post('/api/leads/submit', LeadsController::class . '@submit');

$router->group(['prefix' => '/admin', 'middleware' => [AuthMiddleware::class]], function (Router $r) {
    $r->get('/users', UsersController::class . '@index');
});
  • {slug} — a parameter (default [^/]+); you can supply your own regex: {id:\d+}.
  • Groups add a shared prefix and middleware.
  • A Class@method handler is lazy: the class is created via the container only when the route matches.

Container and auto-injection

The Container builds objects by reflection: it inspects the constructor argument types and supplies the needed services. So a controller simply declares its dependencies:

final class MyController
{
    public function __construct(
        private readonly Engine $engine,
        private readonly Options $options,
    ) {}

    public function show(Request $request, array $params): Response
    {
        $this->engine->setTheme((string) $this->options->get('theme', 'default'));
        return Response::html($this->engine->render('my-template', ['x' => 1]));
    }
}

Registrations: bind() — a new instance per request, singleton() — one per application, instance() — a pre-built object.

Response

The response object is built via factories:

Response::html($html);            // text/html
Response::json($data);            // application/json
Response::redirect('/url');       // 302
Response::notFound();             // 404
Response::text('ok');             // text/plain

Error handling

In production (APP_ENV=production) the stack trace is not sent out — the user sees "500 Server Error" while details are written to storage/logs. In dev mode the full trace is shown. This behaviour is enforced: even with APP_DEBUG=true accidentally left on in production, the trace won't leak.