0

The Application class — bootstrap sequence

Advanced5 min read·fw-01-003
laravel-src

Concept

The Application class is the most important class in the entire framework. It is the first object created on every request and the last one destroyed. Every other component is either registered into it or resolved from it. Getting its design right — especially its bootstrap sequence — determines whether your framework is pleasant to extend or a nightmare to debug.

The bootstrap sequence is the ordered series of operations that transform a blank PHP process into a ready-to-handle-requests application. Laravel's sequence, defined in Illuminate/Foundation/Application.php and executed by the HTTP Kernel, is: register base bindings → register service providers → boot service providers → handle the request. Each step has strict ordering rules: you cannot boot a provider before all providers are registered, because boot() is allowed to call app()->make() and expects all bindings to already exist.

Our Application class will be responsible for three things: path resolution (covered in lesson 2), container management (we will wire this in during fw-02), and the bootstrap sequence. For now, we implement the bootstrap scaffolding — a series of registerXxx() methods called in explicit order from the constructor. This explicit ordering is different from Laravel's deferred-provider system, but it is much easier to reason about when learning.

The Application class is also where you handle the two lifecycle events that PHP provides before your own code runs: set_exception_handler() registers a callback for uncaught exceptions, and register_shutdown_function() registers a callback for fatal errors that bypass the exception system entirely. Both must be registered during bootstrap, before any request handling begins. Laravel registers these in Illuminate\Foundation\Bootstrap\HandleExceptions.

A key design decision: should the Application class inherit from the Container? Laravel does this, making $app->make() available directly. We will not — we will keep Application and Container as separate objects with the Application holding a reference to the Container. This makes the code more readable for learning purposes, at the cost of a tiny indirection.

Code Example

php
<?php
declare(strict_types=1);

namespace Lumen\Foundation;

use Lumen\Container\Container;

/**
 * The Application orchestrates the framework's bootstrap sequence.
 *
 * Bootstrap order is critical and must not change:
 * 1. Resolve base paths
 * 2. Create and configure the container
 * 3. Register core framework bindings
 * 4. Load configuration
 * 5. Register error/exception handlers
 *
 * Laravel equivalent: Illuminate\Foundation\Application
 */
class Application
{
    public const VERSION = '1.0.0';

    private static ?self $instance = null;

    private Container $container;

    private bool $booted = false;

    /** @var list<callable> Callbacks invoked during boot. */
    private array $bootingCallbacks = [];

    /** @var list<callable> Callbacks invoked after boot. */
    private array $bootedCallbacks = [];

    public function __construct(
        private readonly string $basePath
    ) {
        static::$instance = $this;

        $this->container = new Container();

        $this->registerBaseBindings();
    }

    /**
     * Register the most fundamental bindings so the container can resolve
     * them immediately. All other bindings are registered by service providers
     * in later boot phases.
     */
    private function registerBaseBindings(): void
    {
        // Allow code to resolve the Application instance from the container
        $this->container->instance('app', $this);
        $this->container->instance(static::class, $this);
        $this->container->instance(Container::class, $this->container);
    }

    /**
     * Boot the application.
     * Must be called once, after all service providers are registered.
     * Subsequent calls are no-ops.
     */
    public function boot(): void
    {
        if ($this->booted) {
            return;
        }

        foreach ($this->bootingCallbacks as $callback) {
            $callback($this);
        }

        $this->booted = true;

        foreach ($this->bootedCallbacks as $callback) {
            $callback($this);
        }
    }

    /** Register a callback to run just before the application boots. */
    public function booting(callable $callback): void
    {
        $this->bootingCallbacks[] = $callback;
    }

    /** Register a callback to run immediately after the application boots. */
    public function booted(callable $callback): void
    {
        if ($this->booted) {
            // If already booted, invoke immediately
            $callback($this);
            return;
        }
        $this->bootedCallbacks[] = $callback;
    }

    public function isBooted(): bool
    {
        return $this->booted;
    }

    public function getContainer(): Container
    {
        return $this->container;
    }

    // ------------------------------------------------------------------
    // Path helpers
    // ------------------------------------------------------------------

    public function basePath(string $path = ''): string
    {
        return $path === ''
            ? $this->basePath
            : $this->basePath . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
    }

    public function configPath(string $path = ''): string
    {
        return $this->basePath('config' . ($path !== '' ? DIRECTORY_SEPARATOR . $path : ''));
    }

    public function storagePath(string $path = ''): string
    {
        return $this->basePath('storage' . ($path !== '' ? DIRECTORY_SEPARATOR . $path : ''));
    }

    public function srcPath(string $path = ''): string
    {
        return $this->basePath('src' . ($path !== '' ? DIRECTORY_SEPARATOR . $path : ''));
    }

    // ------------------------------------------------------------------
    // Global instance access (last resort — prefer constructor injection)
    // ------------------------------------------------------------------

    public static function getInstance(): static
    {
        if (static::$instance === null) {
            throw new \RuntimeException(
                'Application has not been instantiated. Call new Application($basePath) first.'
            );
        }
        return static::$instance;
    }

    public function version(): string
    {
        return static::VERSION;
    }
}

Interview Q&A

Q: Why does the bootstrap sequence matter — what goes wrong if you change the order?

Order violations cause "class not found" or "binding not registered" errors that are frustratingly hard to debug because the stack trace points to the caller, not the registration code. The classic example: if you boot service providers before all providers are registered, a provider's boot() method may call $app->make(SomeInterface::class) which has not yet been bound. You get a BindingResolutionException. Laravel solves this by separating register() (where bindings are added) from boot() (where the application object is used), and calling all register() methods before any boot() methods. Our bootingCallbacks / bootedCallbacks system gives the same guarantee — everything registered via booting() fires before the booted flag is set.


Q: What is the difference between booting() and booted() callbacks?

booting() callbacks fire before the $this->booted = true flag is set. They are the last chance to register or modify bindings before anything considers the app "ready." booted() callbacks fire after the flag is set. If booted() is called on an already-booted application, the callback is invoked immediately — this supports the pattern of registering late callbacks from outside the bootstrap sequence (e.g., in tests). In Laravel, service providers implement boot() which is called as a booting callback. The AfterResolving and similar hooks are effectively booted callbacks.


Q: Laravel's Application extends the Container. Why might our approach of keeping them separate be preferable for a learning framework?

When Application extends Container, $app IS-A container, which means the Application class inherits over 40 public methods from Container. Reading the Application class becomes hard — you have to know which methods come from Application and which come from Container. For learning, we prefer composition: the Application HAS-A container, and you can read the Container chapter independently. The trade-off is that you must write $app->getContainer()->make(...) instead of $app->make(...). Laravel adds __call and explicit proxy methods on Application to bridge this. In production frameworks, the Laravel approach is standard — the coupling is intentional and documented.