Learning Laravel - Observations, part 1: The service container

Posted on by Matthias Noback

With excerpts from the documentation

I have worked with symfony 1, Symfony 2+, Zend Framework 1, Zend Expressive, but never with Laravel. Until now. My partner was looking for a way to learn PHP and building web applications with it. Most of my own framework knowledge is related to Symfony, so my initial plan was to find a Symfony course for her. However, Jeffrey Way's Laracasts also came to mind, and I thought it would be an interesting learning experience for us both if Laravel would be the framework of choice in this matter. It turned out to be a good idea. My partner is making good progress, and I get to see what Laravel is all about. We have a lot of fun together, finding out how it works, or what you're supposed to be doing with it.

As a side-project, I've been reading the official Laravel documentation. Being a human with framework habits, I couldn't help but compare the Laravel approach to the Symfony approach. I've also compared some of the suggestions from the documentation with what I think are best practices for any web application, regardless the framework that you choose. Something to keep in mind when reading this article is that my approach to web application architecture is to keep the framework and other infrastructural concerns far from the code that represent my application's use cases and the domain models it contains. See also the series I wrote about this approach earlier (part 1, part 2 and part 3). I'm usually not looking for a way to develop something as quick as possible, or make development as convenient as possible. I try to find ways to protect its future against external influences, like changes in the language, the framework, a desire to switch to a different database, queueing system, etc. This will certainly color some of my observations in this and following articles, including the advice I give here on which feature to use, and which ones to ignore.

I also want to make clear that I'm absolutely not here to bash Laravel or anything. Quite the contrary actually, I think its builders have made some great decisions. They have also added things that I guess might nudge people in the wrong direction in terms of object design, but keep in mind: this is all my opinion. Of course, I try to give proper arguments, but in the end I'm not here to say: use Symfony, ditch Laravel. If anything, my message when it comes to frameworks would be: use them to your advantage, but only in the parts of your code base where it makes sense. Don't let them determine your overall design (or architecture), make sure you can swap parts of them out when you want or need to. If Laravel/Symfony/Zend/etc. suits your needs today, use it today, but prepare for the day when you don't want to use it anymore.

With all disclaimers out of the way, let's go!

The service container

Let's start with a discussion about the service container. After I wrote the article on Hand-written service containers someone pointed out to me that my solution wasn't the only one that has the advantage of being easy to refactor; the classes, interfaces, and methods that you use in Laravel service definitions are also indexable by your IDE and are therefore easy to rename, move, etc. That's totally correct. This is what a service definition looks like:

// Defining a service inside a closure:

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

// Using it somewhere else:

$this->app->make('Helpspot\Api');

// Note: you can use `Api::class` instead of spelling out the full class name

However, most services don't even need all this work, because you can give the container instructions. E.g. instead of binding an interface with a closure, you can also bind it with a class name, so that, whenever an object requires an instance of the interface, it will get an instance of that class injected:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

Shared versus singleton services

I think it's very interesting that by default the services that you define using bind() are not shared, that is, the next time we'd call $this->app->make(), you will get a fresh instance. For Symfony, the default setting is the opposite: when you define a service, it's going to be shared. I definitely prefer the Laravel approach. For two reasons: first, services aren't objects that need reference equality; you shouldn't be worried about the exact instance of a service that you retrieve. You should only care about the fact that it is an instance of the requested class or interface. Second, creating a fresh copy for a service will prevent you from making the service stateful. Keeping state inside the service doesn't make sense, since you can't rely on that state to be there the next time you request that service. (Actually, I'm not sure if I'm projecting my ideals onto the Laravel service container now; maybe not having "singleton" services by default doesn't keep people from writing stateful services, I don't know.)

By the way, it's really cool how the first example of dependency injection in this documentation section is a UserController which needs a UserRepository. This is quite surprising to me, since for a very long time, Symfony developers have been debating whether or not to use constructor injection for controllers. I've always liked the idea, because I've always followed the rule that services should have all their dependencies injected as constructor arguments. However, with Symfony this has been problematic and the established practice has just been to grab services from the service container directly, whenever needed (but only in the controller; more about that later).

Anyway, what I think is a good rule for services is that only the ones that represent a shared resource (like a database connection) should be "singleton" services, and that all the others should be created whenever they are needed, without reusing existing instances. By the way, "singleton" service is a peculiar name here. I like it because it conveys the spirit that there is supposed to be only one instance, like with the Singleton design pattern, but since it does not require the class to implement that pattern, it's a bit confusing too. I just hope that you don't use language constructs to make a class by definition a singleton, but that you'll rely on the service container to make sure only one instance gets created.

Something else to mention in the context of "binding": if a service needs a configuration value to be provided as a string (or some other primitive-type value), you can bind this value too:

$this->app->when('HelpSpot\API')
          ->needs('$apiKey')
          ->give('secret');

This way, the service container will be able to construct the object, providing 'secret' as the constructor argument for a parameter called $apiKey. Symfony has this same option for helping the service container find the right value for a primitive-type parameter. I don't like it. One problem for me is the fact that something that was previously internal to the class - the name of a constructor parameter - is now also used in some other place. This means that where it was previously completely safe to rename the parameter, now you have to also update the service bindings. A solution for this would be to define dedicated value objects for each configuration value and make them available as "services" too:

namespace HelpSpot;

/*
 * This class can be used to create an object which represents
 * the API key previously passed to the API services as a string:
 */
final class HelpSpotApiKey
{
    private $apiKey;

    private function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public static function fromString(string $apiKey): self
    {
         return new self($apiKey);
    }

    public function asString(): string
    {
        return $this->apiKey;
    }
}

final class API
{
    /*
     * We no longer rely on a string, but on a `HelpSpotApiKey` 
     * value object:
     */
    public function __construct(HelpSpotApiKey $apiKey)
    {
        // ...

        // to use the API key:
        $apiKey->asString();
    }
}

/*
 * As soon as we bind the `HelpSpotApiKey` value object, Laravel will
 * be able to instantiate the `API` service all by itself:
 */ 
$this->app->bind(HelpSpotApiKey::class, function ($app) {
    return HelpSpotApiKey::fromString('secret');
});

Note: if you feel like writing classes like HelpSpotApiKey is a lot of work; I completely agree. If you want, create a little trait that saves you from writing all that code by hand. The added benefit to me is definitely worth the exta classes.

There are some other service container configuration options that seem quite useful, e.g. the ability to bind an interface to different classes, based on which service requires it (Contextual binding), and another one that is very powerful - the ability to tag services and retrieve all services with a given tag. Symfony has this too, and I've always found it an amazing feature. The major difference here is that, instead of dealing with services during the container compilation phase (as is done in Symfony), with Laravel you can just grab the tagged services. I remember dealing with tagged services in Symfony for the first time was a real mind-bender. With Laravel it is more intuitive, for sure:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

Instantiating services

The problems for me start with the ability to use the bootstrapped application to resolve services using its make() method:

$api = $this->app->make('HelpSpot\API');

This is a power that belongs not to the user, but to the framework. The only place where make() should have to be called is inside the framework kernel, after it has found out which controller it should call, based on the available HTTP request information (e.g. the route, its parameters, the host name, etc.). At that point, the kernel will instantiate the controller. The controller will have all its dependencies injected as controller arguments, and any of those services will have its dependencies injected as constructor arguments too. That way, calling the controller will indirectly produce a number of calls to the service's dependencies, but along the way, no service will have to resolve more dependencies on-the-spot.

Dynamically resolving dependencies has always been considered a design problem anyway, for many reasons. It hides the actual dependencies, making it difficult to get a clear picture of what those dependencies are, and consequently, of what the job of a service is. Being able to look at a constructor and see what its dependencies are is a great way of getting to know a service. I wouldn't want to read the code in detail to find out what a service does. The public API of a service (the methods, including the constructor, their parameter types, and their return values) should tell me all I need to know.

For this reason, I'm not particularly happy about the suggestion in the documentation that "If you are in a location of your code that does not have access to the $app variable, you may use the global resolve helper":

$api = resolve('HelpSpot\API');

Static dependency instantiation

There's another problem with using static functions for resolving services (and some framework use them for resolving configuration too). They introduce framework coupling. It's very likely that in just one or two years, things will be completely different, and we no longer should (or can't) use this resolve() function anymore. Maybe we want to switch to a different framework, maybe the team agrees that they don't want to resolve dependencies on the spot. Then you're stuck with them.

I've recently had this experience with Zend Framework 1's Zend_Registry which is used to share services and configuration across a code base. Although Zend_Registry if more like a globally available map of things, and by far not an auto-wiring service container, it's still the same pattern. Application code will have lots of calls like Zend_Registry::get('Zend_Translate') and the likes, to retrieve globally available services. When using resolve(), or even $this->app->make(), you'll end up with the problem that you're not only depending on a service, you're now also depending on the service you need for retrieving that service. And this is a really painful thing. As an example of how this becomes very impractical: if your application uses Zend_Registry::get('Zend_Translate') to retrieve the translator service, and you now want to use the Symfony service container in your project, you'll either have to do something sneaky with Zend_Registry to make that work (but then you'll still depend on Zend_Registry, which you wanted to get rid of), or you'll have to rewrite all the code to get the translator injected, which will be a lot of work. In legacy migration projects, the first solution will be chosen, but this is far from ideal, and eventually you'll have to rewrite everything to use proper constructor dependency injection anyway.

Rewriting to constructor injection is mainly a problem if the instantiation of a class is not completely under your control. E.g. when the framework instantiates your controller (like Symfony does, if it isn't defined as a service). It will just be new $controllerClass(), no arguments provided. In such cases, instead of giving up the possibility to inject dependencies, try to get control over the instantiation of your classes again, so you can also use any of the dependency injection benefits that your framework has to offer.

So, this is why my advice is: don't use any of these seemingly convenient shortcuts. Just use constructor injection always. Anyway, why is constructor injection considered to be so very painful that Laravel provides us shortcuts for it? It already has something very cool that makes manualy make()-ing services obsolete: Automatic Injection.

Automatic dependency injection

As you may know, the difference between a framework and a library is: you call a library, but the framework calls you. At the crucial moment that it does this, e.g. when it calls your controller, the Laravel service container will set up your controller by automatically injecting all the dependencies that it needs. As I mentioned earlier, this is how I think it should be. It also means that, really, there is no need for you to call the service container yourself, to resolve dependencies for you. You only have to make sure that they are already injected as constructor arguments.

Something I find weird is the possibility to injection dependencies as regular method arguments. The documentation mentions the handle() method of queued jobs. It looks like a job is a class which combines both the payload and the logic for processing that payload in a single class. This is where the need to inject dependencies as method arguments comes from. This need would completely disappear if you would simply separate these two things, i.e. have a class for the payload, and another class to process that payload. The first class will be a simple data transfer object, with no behavior at all, because it will just serve as a means for passing the data from the backend application to the queue worker. The second class will be a proper service, with its dependencies injected as constructor arguments. In my book (literally ;)), you can't combine a data holder and a service object in one class. But it's a matter of style after all.

Manipulating services after they have been instantiated

Using something called container events the service container makes it possible for you to manipulate a service after it has been instantiated:

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // Called when container resolves objects of type "HelpSpot\API"...
});

The documentation mentions: "As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consumer."

Implicitly, this means that services can be designed to be mutable. A service, once instantiated, can be reconfigured and to start behaving differently. This is a design style I don't recommend, since the service that gets injected into other services, is no longer a predictable thing. Any client that has access to it can change its behavior. Of course, you could say: that won't happen. But it happens, and leads to very confusing problems. I think it's a great idea to make services immutable; provide every dependency they need as a constructor argument, and it should be possible for it to run like machine, forever.

I find that usually the need to inject things into a service after construction time is caused be two issues that can easily be solved.

  1. It may seem more convenient to call setters multiple times, than to prepare a data structure for the constructor. In that case: just take that tiny bit of extra time.
  2. Data ends up being set on the service that is not strictly a dependency or a configuration value, but is in fact contextual data, and therefore can't be resolved by the container all by itself. For example, maybe you want to inject into the service the current user's IP address, or the request URI. Or maybe even the entire session object. This kind of data should not be injected, but passed to it as a regular method argument. I've described this in more detail in another article about Context passing.

Conclusion

Laravel's service container looks great. I like the idea that it can figure things out mostly by itself. I like that it's PHP-based, and that its syntax is quite compact. I think that most of the convenience functions (e.g. resolve()) and exotic options (like $this->app->resolving()) should be ignored. The best thing you can do for your application in the long term is to let all your services use dependency injection, and to inject only constructor arguments. This keeps things simple, but also portable to other frameworks with other dependency injection containers, or other architectural styles, when the time is there.

PHP Laravel
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Yann Rabiller

Services are indeed not shared by default, but it seems that Laravel's Facades are singleton (although it's not written anywhere): https://github.com/laravel/...

David Négrier

Hey Matthias! Great article, as always.

I see a third use case where "manipulating services after they have been instantiated" makes sense.

Sometimes, you have "loops" in services dependencies, where service A requires service B, which requires service C which requires again service A.

While I'm sure you will tell that this is usually a bad design decision, there are very rare cases where this can be actually needed. I encountered such a use case last month while working on GraphQLite (my GraphQL server library) where a "fields builder" calls several "object resolvers" which can in turn need the "fields builder" (because of the recursive nature of GraphQL resolving)

With constructor injection, these loops are impossible. Having a way to alter a service after it is created (to call a setter to reference the "parent" service) is really the only proper way of doing things I found (and god knows I fought hard to avoid this loop).

Rudolph Gottesheim

I would suggest breaking those kinds of circular dependencies with a Lazy* implementation of one of the services in the circle.

Davide Borsatto

Is Laravel's container still built on-the-fly on every request? It was one of my main issues with it.

The good thing about Symfony's compiled container approach is that the performance impact is extremely low, and it's also easier to debug as you can just inspect the dumped container.

Xander

Hi I'm not sure what your use cases are, but the performance has never been an issue for me. The container and services and used classes are being cached. Same goes for the available route config and application configuration.
For debugging it has a decent facility called Telescope:
https://laravel.com/docs/5....
Also when it comes to queue monitoring:
https://laravel.com/docs/5....