Request lifecycle
Request lifecycle
What happens from the moment the browser knocks on the site to the moment HTML is returned.
Step by step
- Web server.
nginxsits in front and serves static files (css/js/img) directly. PHP requests are proxied to Apache/PHP-FPM, which — via.htaccess— routes everything toindex.php(front) oradmin.php(admin).
- Entry point.
index.phpchecks forinstall.lock(otherwise it redirects to the installer) and includescore/bootstrap.php.
- Bootstrap + Kernel.
core/bootstrap.phpcreates theKernel, which:- loads the
Autoloader(PSR-4); - builds the DI
Container; - reads
Configfrom.envandconfig/*.php; - registers services:
Connection(DB, lazy),Engine(templates),Router,Logger,Locale,Translator,Auth,Optionsand others; - starts the session (unless CLI).
- loads the
- Locale. The active language is detected (
Locale::detect():?lang→ cookie →Accept-Language→ default) and set on theTranslator.
- Routes.
bootstrap.phpregisters public and admin routes. Finally the active plugins are loaded (PluginManager::load()) — theirplugin.phpfiles add their own routes, hooks and tags.
- Dispatch.
Kernel::handle($request)callsRouter::dispatch(). The router matches the path against route patterns and finds the handler.
- Middleware. If the route has middleware (e.g.
AuthMiddlewarefor/admin/*), the request passes through them.
- Controller. The router resolves the controller from the container (with dependency auto-injection) and calls the
(Request $request, array $params)method. The controller returns aResponse.
- 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@methodhandler 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.