Lifecycle & Bootstrap

What the Kernel itself does from the moment you call Kernel::init() — the shared bootstrap every Forge program goes through.

Kernel::init()

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 code

This 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.

The Boot Sequence

Once Kernel::init() starts, the bootstrap runs through a small, predictable set of steps:

  1. Storage folders — makes sure the storage/ directories exist (sessions, logs, database, framework cache, app, bin, queues), creating any that are missing.
  2. Cache trigger — flags caches for a rebuild when the source or structure has changed.
  3. Environment — sets up the environment object that your env() calls read from.
  4. Timezone — applies APP_TIMEZONE, or UTC if it isn't set.
  5. Session defaults — if a session provider is available, the Kernel hardens the session cookie (HttpOnly, Secure, SameSite, strict mode). Not something you configure by hand.
  6. Container setup — registers the core services (config, application, module loader), loads modules, discovers services, and runs the lifecycle hooks.
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.

CLI Flow

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 commandsnonestructure:init, storage:link, generate:module
Modules & capabilitiesmodules:modules:forge-router:init
Your app (via forge_structure.php)nonewhatever you name it
#[CoreCommand]nonedb:migrate
Development-onlydev: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.

How Modules Load

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 { ... }
  • Ordering. Modules load in ascending order. Lower numbers first.
  • Core vs the rest. Core-type modules load separately, after the normal ones.
  • Disabled modules. You can switch a module off with DISABLED_MODULES in .env (or the config), and the loader skips it.
  • Caching. The Kernel caches discovered modules and compiled hooks under storage/framework/cache/, and rebuilds them when something changes — so normal requests don't re-scan your folders.
  • Registering behavior. Loading a module reads its attributes (config defaults, structure, provides, requires, hooks, services) and runs any 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.

Service Discovery

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\*  registered
  • No attribute needed. Put a class in an injectable folder and it's found. Type-hint it in a constructor and it's injected.
  • #[\Injectable] is optional. It's a leftover from before the refactor. You only need it if you want a custom id or a non-singleton.
  • Only those folders. That's the point of being scoped — the Kernel doesn't crawl everything, which keeps boot fast.
  • Lifecycle hooks in discovered services. While scanning, the Kernel also picks up #[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.

Config Loading

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 array

Modules 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.

Lifecycle Hooks

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_BOOTFirst thing after early-hook discovery, before modules load.
BEFORE_MODULE_LOADRight before modules are loaded into the container.
AFTER_MODULE_LOADAfter all modules have been loaded.
AFTER_MODULE_REGISTERRight after each individual module registers.
AFTER_CONFIG_LOADEDOnce config is loaded.
APP_BOOTEDThe 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.