What the Kernel itself does from the moment you call Kernel::init() — the shared bootstrap every Forge program goes through.
The whole Kernel entry point is one static call: Kernel::init(). It loads your environment if a .env is present, then kicks off the bootstrap. That's it. Whatever kind of program you're building — a web app, a background worker, a CLI tool — it all starts here.
Kernel::init()
→ load .env # if a .env file exists
→ Bootstrap::getInstance()
→ container setup, modules, services, hooks
→ control returns to your codeThis is deliberately small. The Kernel doesn't know or care why you started a program — it only knows how to bring a Forge app to life and hand control back.
This page is about the Kernel only. HTTP routing, dispatching, and the request/response cycle aren't Kernel responsibilities — they belong to a capability like ForgeRouter, which documents its own lifecycle on its own page. Here we cover the Kernel's bootstrap: container, modules, services, config, and the lifecycle hooks it exposes.
Once Kernel::init() starts, the bootstrap runs through a small, predictable set of steps:
storage/ directories exist (sessions, logs, database, framework cache, app, bin, queues), creating any that are missing.env() calls read from.APP_TIMEZONE, or UTC if it isn't set.Bootstrap::getInstance()
→ ensure storage dirs
→ process cache trigger
→ load environment, timezone
→ session defaults (if a provider is available)
→ ContainerAppSetup::setup()
→ register core services (Config, Application, Loader)
→ load helper includes
→ discover early hooks
→ trigger EARLY_BOOT hook
→ load modules (ModuleSetup)
→ set up error handler
→ discover services (ServiceDiscoverSetup)
→ trigger APP_BOOTED hook
→ finish bootstrap (enable cache wrapping)That last step — container setup — is where the interesting parts happen. It's covered section by section below.
The CLI is the Kernel's own driver — forge.php boots the same core, then hands control to the command dispatcher. This is a concrete, always-present example of the bootstrap in action.
forge.php
→ define BASE_PATH, register Autoloader
→ load .env
→ Bootstrap::initCliContainer()
→ register Config
→ ContainerCLISetup::setup()
→ load helper includes
→ set up session + error handler
→ register Application singleton
→ load modules + preload CLI modules
→ discover services
→ discover commands (AppCommandSetup)
→ Application::run($argv)
→ find the command you named
→ run it (or show help / interactive browser)Commands are grouped by prefix, which is purely about how they're organized and typed — it has nothing to do with whether something is part of the Kernel. The conventions:
| Registered by | Prefix | Example |
|---|---|---|
| Core Kernel commands | none | structure:init, storage:link, generate:module |
| Modules & capabilities | modules: | modules:forge-router:init |
Your app (via forge_structure.php) | none | whatever you name it |
#[CoreCommand] | none | db:migrate |
| Development-only | dev: | dev:module:list |
A command registered by a module normally gets a modules: prefix. Add the #[CoreCommand] attribute to the command class and that prefix is skipped — the command keeps its full name, so it's short and easy to type on its own, like db:migrate. It's a naming decision, not a statement that the command is part of the Kernel. App commands you list in forge_structure.php are registered without a prefix from the start.
The dev: commands only appear when FORGE_DEVELOPER_MODE=true is set in .env. They exist mostly for Kernel and module development — tools to inspect packages, blueprints, and structure — so you can read them to understand how things are put together, but you won't usually need them day to day.
Try it: run php forge.php with no arguments to get the interactive startup — it offers the command list or an interactive browser. php forge.php list shows all commands directly.
During bootstrap, the Kernel discovers your modules and capabilities and brings them into the container in a deliberate order.
It scans each module root (modules/ and capabilities/, as configured) and looks for folders containing an entry file like src/ForgeRouterModule.php. That entry file is what tells the Kernel a folder is a module. It then reads the module's #[Module] attribute — which carries metadata like its order, type, and whether it's a core module — and sorts by order before loading.
#[Module(name: "ForgeRouter", type: "core", order: PHP_INT_MAX)]
final class ForgeRouterModule { ... }order. Lower numbers first.DISABLED_MODULES in .env (or the config), and the loader skips it.storage/framework/cache/, and rebuilds them when something changes — so normal requests don't re-scan your folders.register() / registerIncludes() / registerCommands() methods it defines.The details of how a specific capability wires itself in live on that capability's own page — here we just care about the sequence the Kernel drives.
After modules load, the Kernel scans a few specific folders to find classes that should be registered with the container so they can be injected anywhere.
That scan is scoped, not a crawl of every folder. It reads the injectable paths from your structure configuration — by default app/Services and app/Listeners, and inside each module src/Services, src/Listeners, and src/Providers. Whatever concrete (non-abstract, non-interface) classes it finds there get registered with the container. Those folders are just defaults — you can point this path anywhere.
# scoped scan at boot
app/Services/* → App\Services\* registered
app/Listeners/* → App\Listeners\* registered
modules/*/src/Services → Modules\*\Services\* registeredinjectable folder and it's found. Type-hint it in a constructor and it's injected.#[LifecycleHook] methods on discovered classes and registers them.A generated class map under storage/framework/cache/ lets the Autoloader resolve classes without scanning the filesystem on every request.
Configuration is simple PHP files. During bootstrap the Kernel reads every *.php file in config/, keyed by filename, into a single Config object. Then config('registry') returns whatever config/registry.php exports.
config('forge_router.cors.allowed_origins'); // nested dot access, one arrayModules can contribute their own defaults through a #[ConfigDefaults] attribute on their module class, which is merged into the config when the module loads. That's how capabilities ship sane defaults you can then override in config/.
For anything sensitive or environment-specific, use .env and the env() helper instead of hard-coding it in a config file.
The Kernel exposes a set of named moments during boot where modules — or your own discovered services — can run code. You declare one by tagging a method with #[LifecycleHook(hook: ...)].
#[LifecycleHook(hook: LifecycleHookName::APP_BOOTED)]
public function boot(): void { /* ... */ }| Hook | When it runs |
|---|---|
EARLY_BOOT | First thing after early-hook discovery, before modules load. |
BEFORE_MODULE_LOAD | Right before modules are loaded into the container. |
AFTER_MODULE_LOAD | After all modules have been loaded. |
AFTER_MODULE_REGISTER | Right after each individual module registers. |
AFTER_CONFIG_LOADED | Once config is loaded. |
APP_BOOTED | The final hook — services are discovered, and your app can do its real work. Whatever happens next is up to your code and any capabilities you've installed. |
Some hooks can run very early — before a module is even fully loaded — which is how modules participate in the bootstrap the moment their entry class is seen, and how the loader knows their early boot and before-module-load callbacks.