The framework boots through PHP_SF\System\Kernel — a configuration and wiring class that registers controllers, translation files, templates, and the application user class before handing off to the router. This page covers what happens during boot, in what order, and how to configure it.
There are three places where the kernel boots, each for a different context:
| File | Context |
|---|---|
public/index.php |
HTTP requests |
bin/console |
Symfony console commands |
tests/bootstrap.php |
PHPUnit and Codeception test suites |
All three follow the same wiring pattern — the differences are only in what happens after the framework kernel is configured.
This is the main entry point for every HTTP request. The full boot sequence is:
// 1. Autoloader
require_once __DIR__ . '/../vendor/autoload.php';
// 4. Symfony debug handler (only when DEV_MODE = true)
if ( DEV_MODE )
Debug::enable();
// 5. Environment variables
( new Dotenv )->bootEnv( __DIR__ . '/../.env' );
// 6. Framework kernel — wire everything together
$kernel = ( new PHP_SF\Kernel() )
->addTranslationFiles( __DIR__ . '/../translations' )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->addEventSubscriberDirectory( __DIR__ . '/../App/EventSubscriber' )
->setHeaderTemplateClassName( header::class )
->setFooterTemplateClassName( footer::class )
->setApplicationUserClassName( User::class )
->addTemplatesDirectory( 'templates', 'App\View' );
// 7. Attempt to log in user from session
auth::logInUser();
// 8. Boot router — finds route and dispatches, or falls through
Router::init( $kernel );
// 9. If no framework route matched, boot Symfony kernel
Kernel::addRoutesToSymfony();
require_once __DIR__ . '/../autoload_runtime.php';
Steps 1–5 happen before the framework kernel is instantiated. Constants and helper functions must be available before anything else because the kernel itself uses them internally.
If Router::init() finds a matching route, it dispatches the request and calls exit() — steps 11 and beyond never execute. Only unmatched requests fall through to Symfony. See Symfony Fallback for details.
PHP_SF\System\Kernel is configured via a fluent builder interface. All methods return $this so they can be chained.
addControllers(string $path): self->addControllers( __DIR__ . '/../App/Http/Controller' )
Registers a directory to scan for controller classes. The router will recursively scan this directory for PHP files containing classes with #[Route] attributes.
You can call this multiple times to register controllers from multiple directories:
$kernel = ( new PHP_SF\Kernel )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->addControllers( __DIR__ . '/../App/Http/AdminController' );
Throws DirectoryNotFoundException if the path doesn't exist.
addTranslationFiles(string $path): self->addTranslationFiles( __DIR__ . '/../translations' )
Registers a directory containing locale YAML files. The directory should contain files named after locale keys — e.g. en.yaml, pl.yaml, uk.yaml. The framework automatically registers its own baseline directory (Platform/lang/) first; calling addTranslationFiles() registers additional directories after it.
TranslatorV2 loads all YAML files for every locale in LANGUAGES_LIST from every registered directory and merges them. Keys from later directories override keys from earlier ones on collision. The last registered directory is where missing keys are written in DEV_MODE:
$kernel = ( new PHP_SF\Kernel )
->addTranslationFiles( __DIR__ . '/../translations' );
// Platform/lang/ (framework baseline) is registered automatically before this
Throws DirectoryNotFoundException if the path doesn't exist.
setApplicationUserClassName(string $className): self->setApplicationUserClassName( User::class )
Registers the application's user entity class. This must implement PHP_SF\System\Interface\UserInterface. The framework uses this class in:
auth middleware — to verify authentication and load the current userauth::logInUser() — to load the user from session on each requestKernel::getApplicationUserClassName() — used internally when the framework needs to instantiate or query the userThrows InvalidConfigurationException if the class doesn't exist. This method must be called — the framework will throw if getApplicationUserClassName() is called before it is set.
setHeaderTemplateClassName(string $className): self->setHeaderTemplateClassName( header::class )
Sets the view class rendered before every non-API response. Defaults to PHP_SF\Templates\Layout\header. Must extend AbstractView.
Can be overridden at runtime from within a middleware using $this->changeHeaderTemplateClassName() — useful for rendering pages with no header (e.g. the blank middleware in the template project swaps both header and footer to an empty view for the welcome page).
setFooterTemplateClassName(string $className): self->setFooterTemplateClassName( footer::class )
Sets the view class rendered after every non-API response. Defaults to PHP_SF\Templates\Layout\footer. Same rules as the header.
addTemplatesDirectory(string $directory, string $namespace): self->addTemplatesDirectory( 'templates', 'App\View' )
Registers a templates directory and its PSR-4 namespace for the template cache system. When TEMPLATES_CACHE_ENABLED = true, the TemplatesCache uses these mappings to locate source files, compile them, and cache the result under a unified namespace.
The directory path is relative to the project root. The namespace must match the PSR-4 namespace declared in the template files.
addEventSubscriberDirectory(string $dir): self->addEventSubscriberDirectory( __DIR__ . '/../App/EventSubscriber' )
Registers a directory to scan for Symfony EventSubscriberInterface implementations. The framework automatically scans its own built-in subscriber directory; call this method to add your application's subscriber directory.
Subscribers are discovered lazily on first dispatch (or on boot in production where the class list is cached via APCu/Redis). Any class implementing EventSubscriberInterface found in the directory tree is registered automatically — no manual registration required.
You can call this method multiple times to register additional directories:
$kernel = ( new PHP_SF\Kernel() )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->addEventSubscriberDirectory( __DIR__ . '/../App/EventSubscriber' )
->addEventSubscriberDirectory( __DIR__ . '/../modules/payments/EventSubscriber' );
See Events & Listeners for how to write subscribers.
The console entry point wires the framework kernel the same way as public/index.php, then boots a Symfony Application on top:
// bin/console (simplified)
require_once __DIR__ . '/../vendor/autoload.php';
( new Dotenv )->bootEnv( __DIR__ . '/../.env' );
$kernel = ( new PHP_SF\Kernel() )
->addTranslationFiles( __DIR__ . '/../translations' )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->setApplicationUserClassName( User::class );
return function () {
$kernel = Kernel::getInstance();
$kernel->boot();
return new Application( $kernel );
};
The framework kernel is booted so that em(), ca(), translation functions, and other framework helpers are available inside console commands. The Symfony Application then takes over and runs the requested command.
The test bootstrap follows the same pattern with two key additions — it calls Router::loadRoutesOnly( $kernel ) to pre-populate the PHP_SF route list (needed by PhpSfRouteLoader for Codeception functional tests), and it sets APP_ENV defaults before bootEnv() so Codeception (which doesn't pre-set APP_ENV) correctly loads .env.test:
// PHPUnit sets APP_ENV=test via phpunit.xml.dist <server> before this runs.
// Codeception does not, so we default it here so bootEnv('.env') picks up .env.test.
$_SERVER['APP_ENV'] ??= 'test';
$_ENV['APP_ENV'] ??= 'test';
( new Dotenv() )->bootEnv( __DIR__ . '/../.env' );
$kernel = ( new PHP_SF\Kernel() )
->addTranslationFiles( __DIR__ . '/../translations' )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->setHeaderTemplateClassName( header::class )
->setFooterTemplateClassName( footer::class )
->setApplicationUserClassName( User::class )
->addTemplatesDirectory( 'templates', 'App\View' );
Router::loadRoutesOnly( $kernel );
auth::logInUser();
$GLOBALS['kernel'] = $kernel;
The kernel instance is stored in $GLOBALS['kernel'] so it's accessible in test classes that need it — for example the MiddlewaresExecutorTest retrieves it via $GLOBALS['kernel'].
Every entry point calls auth::logInUser() with no arguments immediately after the kernel boots. This attempts to restore the authenticated user from the current session:
auth::logInUser();
When called with no arguments it checks the session for session_user_id, and if found, loads the corresponding user entity from the database and stores it in auth::$user. If no session exists or the user isn't found, it does nothing — the request continues as unauthenticated.
This must happen after the kernel boots because it needs em() (the entity manager) to load the user, which in turn requires the Doctrine connection to be available.
Once booted, the Symfony kernel instance is available anywhere via:
App\Kernel::getInstance()
This returns the same instance that was created during boot. It's used internally by the framework to access the Symfony container, router, and other Symfony services when needed — for example routeLink() falls back to the Symfony router for routes not in the framework's own route list.
Missing setApplicationUserClassName() — the framework throws InvalidConfigurationException with a clear message if this isn't set before auth middleware or auth::logInUser() is called.
Event subscriber directory not registered — if addEventSubscriberDirectory() isn't called with your application's subscriber directory, no subscribers in that directory will be discovered. No error, no warning. This is the first thing to check when a subscriber appears to do nothing.
Stale subscriber cache in production — in production, the discovered subscriber class list is cached. If a new subscriber isn't firing after deployment, run app:cache:clear.
Loading order in public/index.php — constants must be loaded before the framework kernel, and the framework kernel must boot before Router::init(). Changing the load order will cause undefined constant errors or missing route registrations.