Laravel Behind the Scenes: Lifecycle - Container

I'm Josip and I've been working with Laravel for quite some time and I've decided to help out others figure out how this amazing framework works. During that process, I'll also show you some cool tricks that are happening behind the scenes. In this series of blog posts, I will try to explain how Laravel works under the hood, from getting a request in index.php to rendering the response back to the client. Since this is pretty advanced and thorough, I will split the guide into couple parts. In this part 1, I will try to explain what happens in index.php and how all necessary services (database connection, event dispatcher, router, ...) are loaded. Next part will be the kernel (handling the HTTP layer of the request), we will dig into Router and we'll wrap up by how does Laravel render the response to the client. For now, let's begin with part 1.

This guide covers the source code of version 5.6 of the Laravel framework.

Very first file loaded

Once you open up your website at http://example.com, your out-of-the-box-configured webserver (nginx, Apache, ...) points to the index.php file located in public/ directory. So this is the very first file on each request you make to the website. Let's open up the index.php file and examine each line.

<?php

define('LARAVEL_START', microtime(true));

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

So let's start...

define('LARAVEL_START', microtime(true));

This line essentialy starts up the timer, so you can time how long it takes to boot up the framework, etc. Fun fact is that this constant is never used throughout entire framework.

require __DIR__.'/../vendor/autoload.php';

This loads the the Composer autoloader file, basically every PHP class is loaded at this point and can be used in your code, there is no need to require MyClass.php. Note that Composer autoloader is not related to anything Laravel.

This is where things get interesting...

$app = require_once __DIR__.'/../bootstrap/app.php';

Let's examine the /bootstrap/app.php file, since this is the file whose returns are being stored in this $app variable.

<?php
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

Creating a new instance of the Application class

Here, we do a couple of things. Basically, we boot up the framework essentials, register the HTTP/Console kernels and return the instance of the application back to the index.php. Note that app.php here serves as an Application factory, that's why it's returning the $app variable, even though the $app is in the global context because of require. Now there is this Illuminate\Foundation\Application class that represents Laravel application instance. This class contains everything from environment, Laravel version, the Container, paths, etc. Once we create a new instance of the that class, we pass on the root directory to the project in the constructor and call couple of methods. Note that Application class extends the Container class. Methods called are:

$this->setBasePath($basePath);
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();

We will examine each of those methods and take a look at the contents of our Application class after each call.

Registering paths in the Container

The setBasePath($rootDirectory) method registers all relevant paths in the Container, such as application (source) path, storage path, resources path, etc. Note that this is easily configurable, you can customize any path you want, for example:

class MyCoolApplication extends Illuminate\Foundation\Application
{
    public function langPath()
    {
        // /languages/*
        return $this->basePath.DIRECTORY_SEPARATOR.'language';
    }

    public function configPath()
    {
        // resources/configuration/*.php
        return $this->resourcesPath().DIRECTORY_SEPARATOR.'configuration';
    }
}

// then new-up this class instead of Laravel's one
$app = new MyCoolApplication(
    realpath(__DIR__.'/../')
);

After we have bound paths, our Application structure looks pretty empty:

Application {#7 ▼
  #basePath: "/myCoolProject"
  #hasBeenBootstrapped: false
  #booted: false
  ...
  #instances: array:9 [▼
    "path" => "/myCoolProject/app"
    "path.base" => "/myCoolProject"
    "path.lang" => "/myCoolProject/resources/lang"
    "path.config" => "/myCoolProject/config"
    "path.public" => "/myCoolProject/public"
    "path.storage" => "/myCoolProject/storage"
    "path.database" => "/myCoolProject/database"
    "path.resources" => "/myCoolProject/resources"
    "path.bootstrap" => "/myCoolProject/bootstrap"
  ]
  #aliases: []
  #abstractAliases: []
  ...
}

As you can see, nothing yet has been loaded, the Container only contains paths. In this moment, any paths can be resolved from the Container using $app->make('path.{what-you-want}').

Registering itself withing the Container

In registerBaseBindings(), we set ourself (ourself = Application class) as a static instance (so we can resolve it using the Singleton design pattern). This is done because we only want ONE global Container. Then, we bind some aliases to ourself, such as app, Container::class (Illuminate\Container\Container) and we also do one specific thing -- binding the package-loader represented by the Illuminate\Foundation\PackageManifest class. Note that this class's constructor does nothing but set up some base paths for itself and store the Filesystem class within itself. No package has been loaded yet. Setting up aliases is done so we can resolve the container from itself by calling app(), app(Container::class) or app('Illuminate\Container\Container').

static::setInstance($this);

$this->instance('app', $this);

$this->instance(Container::class, $this);

$this->instance(PackageManifest::class, new PackageManifest(
    new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));

We can even guess what the contents of our Container will be like right now... It will contain paths, app, Container::class and PackageManifest::class. Let's take a look:

Application {#7 ▼
  ...
  #instances: array:12 [▼
    "path" => "/myCoolProject/app"
    "path.base" => "/Users/josip/Code/poslovi"
    "path.lang" => "/myCoolProject/resources/lang"
    "path.config" => "/myCoolProject/config"
    "path.public" => "/myCoolProject/public"
    "path.storage" => "/myCoolProject/storage"
    "path.database" => "/myCoolProject/database"
    "path.resources" => "/myCoolProject/resources"
    "path.bootstrap" => "/myCoolProject/bootstrap"
    "app" => Application {#7}
    "Illuminate\Container\Container" => Application {#7}
    "Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶}
  ]
  #aliases: []
  #abstractAliases: []
  ...
}

Now, we have registered ourselves in the Container so we can resolve global instance whenever we want it. Next in the lifecycle is registering the core service providers.

Registering core service providers

$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));

These are 3 core services we need to have first. Let's take a look at the register($provider) method.

// This means nothing for us since none of these providers are registered yet
if (($registered = $this->getProvider($provider)) &amp;&amp; ! $force) {
    return $registered;
}

// This also means nothing since we pass instance of the class, not the classname
if (is_string($provider)) {
    $provider = $this->resolveProvider($provider);
}

// We call register() method on each of our providers.
if (method_exists($provider, 'register')) {
    $provider->register();
}

// This means nothing for us, none of our classes have these properties
if (property_exists($provider, 'bindings')) {
    foreach ($provider->bindings as $key => $value) {
        $this->bind($key, $value);
    }
}

// This means nothing for us, none of our classes have these properties
if (property_exists($provider, 'singletons')) {
    foreach ($provider->singletons as $key => $value) {
        $this->singleton($key, $value);
    }
}

// We set this service provider as registered so we don't load it twice
$this->markAsRegistered($provider);

// Means nothing for us, since we're not booted yet
if ($this->booted) {
    $this->bootProvider($provider);
}

return $provider; // Means nothing for us

Event and logging service providers just bind a singleton instance of their concrete implementation in the Container (Event dispatcher and Logger), while routing provider boots up and configures the Router and includes some necesarry services such as URL generator, redirector and controller dispatcher. Router will be a separate lesson since it is a complicated part of the framework. Taking a look at the Container, we can see that core service providers have been loaded. Note that your route files are not yet loaded, we're just configuring everything that's necessary for routing the request. Your request is not yet being routed.

Application {#7 ▼
  ...
  #serviceProviders: array:3 [▼
    0 => EventServiceProvider {#10 ▶}
    1 => LogServiceProvider {#13 ▶}
    2 => RoutingServiceProvider {#16 ▶}
  ]
  ...
  #bindings: array:9 [▼
    "events" => array:2 [▶]
    "log" => array:2 [▶]
    "router" => array:2 [▶]
    "url" => array:2 [▶]
    "redirect" => array:2 [▶]
    "Psr\Http\Message\ServerRequestInterface" => array:2 [▶]
    "Psr\Http\Message\ResponseInterface" => array:2 [▶]
    "Illuminate\Contracts\Routing\ResponseFactory" => array:2 [▶]
    "Illuminate\Routing\Contracts\ControllerDispatcher" => array:2 [▶]
  ]
  #methodBindings: []
  #instances: array:12 [▼
    "path" => "/myCoolProject/app"
    "path.base" => "/myCoolProject"
    "path.lang" => "/myCoolProject/resources/lang"
    "path.config" => "/myCoolProject/config"
    "path.public" => "/myCoolProject/public"
    "path.storage" => "/myCoolProject/storage"
    "path.database" => "/myCoolProject/database"
    "path.resources" => "/myCoolProject/resources"
    "path.bootstrap" => "/myCoolProject/bootstrap"
    "app" => Application {#7}
    "Illuminate\Container\Container" => Application {#7}
    "Illuminate\Foundation\PackageManifest" => PackageManifest {#8 ▶}
  ]
  ...
}

Register core classes

After we have loaded Logger, Router and Event dispatcher, everything else can now be registered. This is what registerCoreContainerAlisases() does. Taking a look at the contents, we can see all services, such as authentication manager, mailer, database, have been loaded. Note that we have not yet even loaded the .env file, the configuration, or instantiated the connection to the database. We're only loading and storing classes in the Container. Everything is being set up. The cool part (parsing the request, connecting to the database, etc.) being performed after, and I will explain why and how in the next part of the series.

Who wants to know more

Since singleton() and instance() are being called a lot throughout the framework, let's take a look at these core methods. singleton() just delegates down to the bind() method with the $shared parameter set to true. This method binds the implementation to the Container which can be resolved whenever needed.

public function bind($abstract, $concrete = null, $shared = false)
{
    // In our case, $share = true

    // If no $concrete is set, just set it to be the same as the $abstract, or just parse down the closure
    // To get the instance
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    // Store the key and the implementation in the $bindings property
    $this->bindings[$abstract] = compact('concrete', 'shared');

    // If the abstract type was already resolved in this container we'll fire the
    // rebound listener so that any objects which have already gotten resolved
    // can have their copy of the object updated via the listener callbacks.
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

Meanwhile, instance() method binds already instantiated class to the Container.

public function instance($abstract, $instance)
{
    $this->removeAbstractAlias($abstract);

    $isBound = $this->bound($abstract);

    unset($this->aliases[$abstract]);

    // We'll check to determine if this type has been bound before, and if it has
    // we will fire the rebound callbacks registered with the container and it
    // can be updated with consuming classes that have gotten resolved here.
    $this->instances[$abstract] = $instance;

    if ($isBound) {
        $this->rebound($abstract);
    }

    return $instance;
}

Note that these methods have the first parameter called $abstract, which is there because we can swap out any concrete instance or implementation for another, but still resolve it with the same key. For example, have the Cache interface and bind the Cache interface whose implementation can be RedisStore, MemcachedStore, FileStore, etc. You can easily swap out the implementations and resolve them with the same Cache::class key.

From Laravel's documentation:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher' // This can be swapped out with any concrete implementation
);

// Resolve RedisEventPusher by calling app(App\Contracts\EventPusher::class)

I finished the Lifecycle series so make sure to check out part 2!

👋 I'm available for software development contract work and consulting. You can find me at [email protected]