This Small Corner

Eric Koyanagi's tech blog. At least it's free!

Laravel Deep Dives: The Service Container

By Eric Koyanagi
Posted on

The Laravel Service Container

The service container is a bit of magic that powers Laravel's dependency injection. You can read more about how to use this key feature here. As with my previous article about routing, I won't be covering the use of this feature. Instead, I want to focus on how it works.

The ServiceContainer class

We know that you can register services (a class or object), usually in the AppServiceProvider class in the "Providers" directory. AppServiceProvider (as the other providers) simply inherits from ServiceContainer, so let's look into this. First, we can look into the configuration file app.php to see where these service providers are defined:

'providers' => ServiceProvider::defaultProviders()->merge([
    /*
     * Package Service Providers...
     */

    /*
     * Application Service Providers...
     */
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    // App\Providers\BroadcastServiceProvider::class,
    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
])->toArray(),

As the comments above this note, you can create your own provider classes, too. That will make more sense as we dive into the details of these ServiceProviders.

Service providers can actually do a lot. Yes, you can obviously register your services, do bootstrapping, register middleware/events, or even publish configuration assets. But where are these classes actually initialized and loaded?

Remember our "public/index.php" file? As discussing in routing, this is what initializes the auto-loader, does application bootstrapping, and finally routes the $request via the Router.php class. Here, we're interesting in the bootstrapping phase. That's where the above classes are initialized:

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

The $app instance also represents the service container (application inherits from Container).

This bootstrapping phase is what creates the $app object, an instance of the Illuminate Foundation "Application" class. This is where the bulk of the application's bootstrapping occurs. We can see this easily in the constructor:

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    $this->registerBaseBindings();
    $this->registerBaseServiceProviders();
    $this->registerCoreContainerAliases();
}

Here, you can see some of the base service containers initialized. Note that it initializes the Event, Logging, and Routing service even if these are commented out in the defaultProviders app configuration (via "registerBaseBindings"). I'm not entirely sure the point of having them as entries in the app array as these aren't "optional".

The providers defined in the app config are initialized within a bootstrap method (at least in Laravel 10! Earlier versions do this differently):

// in "bootstrapWith" in Application.php
$this->make($bootstrapper)->bootstrap($this);
// in class "RegisterProviders" (part of Illuminate's bootstrap subdirectory)
$app->registerConfiguredProviders();

As you can see back in Application.php, this is where the providers we defined in the app config are initialized. It does this like this:

$providers = Collection::make($this->make('config')->get('app.providers'))
                ->partition(fn ($provider) => str_starts_with($provider, 'Illuminate\\'));

$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
            ->load($providers->collapse()->toArray());

The ProviderRepository

The ProviderRepository's load method (as invoked above) is clearly documented with comments, so let's look at what it does:

// First we will load the service manifest, which contains information on all
// service providers registered with the application and which services it
// provides. This is used to know which services are "deferred" loaders.
if ($this->shouldRecompile($manifest, $providers)) {
    $manifest = $this->compileManifest($providers);
}

// Next, we will register events to load the providers for each of the events
// that it has requested. This allows the service provider to defer itself
// while still getting automatically loaded when a certain event occurs.
foreach ($manifest['when'] as $provider => $events) {
    $this->registerLoadEvents($provider, $events);
}

// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
    $this->app->register($provider);
}

Let's look at the $app->register method in more detail (see the Github reference here).

Registering a Service Provider

First, it simply checks if the provider is already registered and returns it (unless it is being force-loaded):

if (($registered = $this->getProvider($provider)) && ! $force) {
    return $registered;
}

Then there's extra logic to parse definitions from a string. This doesn't seem super useful to me, but it is added as a convenience for devs that define these containers as strings instead of as class references.

This sort of convenience does have a cost of course, as looking into this low-level code shows more clearly. So don't use strings, since it involves extra steps!

if (is_string($provider)) {
    $provider = $this->resolveProvider($provider);
}

Finally, we rely on the Provider "register" method:

$provider->register();

// If there are bindings / singletons set as properties on the provider we
// will spin through them and register them with the application, which
// serves as a convenience layer while registering a lot of bindings.
if (property_exists($provider, 'bindings')) {
    foreach ($provider->bindings as $key => $value) {
        $this->bind($key, $value);
    }
}

if (property_exists($provider, 'singletons')) {
    foreach ($provider->singletons as $key => $value) {
        $key = is_int($key) ? $value : $key;

        $this->singleton($key, $value);
    }
}

$this->markAsRegistered($provider);

// If the application has already booted, we will call this boot method on
// the provider class so it has an opportunity to do its boot logic and
// will be ready for any usage by this developer's application logic.
if ($this->isBooted()) {
    $this->bootProvider($provider);
}

return $provider;

Here, we iterate any properties or singletons on the provider and bind each of them. We finally call "register", where user-added logic often lives (e.g. in AppServiceProvider).

Finally, as noted we can have a custom boot process in any provider. The last part of this logic makes sure that these boot methods are invoked even if the app has already booted. Of course, logic in the boot method of a ServiceContainer must be well considered, because this logic lives far "up" in the Application life. For any app, you only want to include absolutely vital logic as the application bootstraps.

I think it's worth noting that all this happens before Laravel touches the "$request" -- yet again underscoring how the request "start time" provided by Laravel is not the complete picture. If you have heavy operations running in the boot phase of your providers, that won't be counted.

Reviewing the bootstrap phase

  1. Providers can be defined in the app config
  2. Service providers are loaded as part of application bootstrapping (even before the $request is created)
  3. This logic mostly lives in illuminate's Foundation Application.php class (created in bootstrapping)
  4. A ProviderRepository calls the container ($app) method to register a ServiceProvider
  5. This in turn calls the "register" method on the service provider, where we can expect user-provided logic

As it's possible to create custom ServiceProviders, hopefully this gives some better background on (at least the basics) of exactly how this works and where to start looking.

The Bind Method

All this explains how the application logic flows into the familiar "register" method where we create our bindings, but doesn't yet explain how the logic provided by ServiceContainers actually works. For this let's look at the example provided by the Laravel docs to understanding a simple binding:

$this->app->bind(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});

We already know that the app property references the Illuminate Application. We're familiar with this class (roughly) from above. We already know it inherits from the Container class.

That's where we need to go to find the implementation of the "bind" method:

public function bind($abstract, $concrete = null, $shared = false)
{
    $this->dropStaleInstances($abstract);

    // If no concrete type was given, we will simply set the concrete type to the
    // abstract type. After that, the concrete type to be registered as shared
    // without being forced to state their classes in both of the parameters.
    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    // If the factory is not a Closure, it means it is just a class name which is
    // bound into this container to the abstract type and we will just wrap it
    // up inside its own Closure to give us more convenience when extending.
    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }

        $concrete = $this->getClosure($abstract, $concrete);
    }

    $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);
    }
}

This shows how the Service Container builds its internal bindings references in a simple associative array keyed by the $abstract (i.e. the class or interface name). Each array value is itself an array created via the nifty compact function.

But how does it know how to utilize these bindings so that a type hint in a constructor "does something"? The answer is (of course) reflection! See the "build" method in the Container. This method creates an instance ReflectionClass.

Reflection isn't something most PHP developers work with often. The basic idea is something developers are usually familiar with, though, which is that you can use reflection to "reflect" into the codebase and peak into its structure (or modify it).

You can do a lot with reflection -- list or invoke methods (even private methods by modifying accessibility!), create new instances, inspect class information, etc. It's a very flexible tool that doesn't always have an obvious use case. Laravel's service container is a great example of how powerful reflection can be, although it's important not to "overdue" it. Few things that are convenient are free. You pay for reflection's flexibility and power in performance.

Going back to the "bind" method in the "Container", we can see how the ReflectionClass is leveraged.

$constructor = $reflector->getConstructor();

// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
    array_pop($this->buildStack);

    return new $concrete;
}

$dependencies = $constructor->getParameters();

// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
try {
    $instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
    array_pop($this->buildStack);

    throw $e;
}

array_pop($this->buildStack);

return $reflector->newInstanceArgs($instances);

All this is still in the context of our bootstrapping phase back in index.php -- so we know that it will resolve all bindings before the $request is even routed. Actually, before the $request is even captured!

Conclusion

  1. The Service Container's logic lives in the app's bootstrapping phase
  2. Service containers are loaded in bootstrapping, and they call a "register" method where most custom bindings live
  3. The bind method uses reflection to resolve dependencies and create instances

There's a lot more to explore with reflection, but now we see how the application creates ServiceContainers, invokes their "register" method, and how the "bind" method in turn uses reflection to resolve all dependencies and create instances.

Beyond becoming more familiar with the specifics of how the ServiceContainer works, this helps remind us why frameworks are so utilitarian. Yes, there's a performance cost, but there's also a wealth of utility in something that is easy to overlook, the Service Container. The opposite point is also valid, that there's a lot of logic happening in Laravel's bootstrapping phase that does indeed have a cost. For 99% of applications that cost is likely acceptable, but for massive scales, these lower-level details can make a big difference.

« Laravel Deep Dives: The Router | Laravel Deep Dives: Queues and Async Design »
« Back to Article List
Written By
Eric Koyanagi

I've been a software engineer for over 15 years, working in both startups and established companies in a range of industries from manufacturing to adtech to e-commerce. Although I love making software, I also enjoy playing video games (especially with my husband) and writing articles.

Article Home | My Portfolio | My LinkedIn
© All Rights Reserved