Laravel: Registering the Manager in the Service Provider

Nothing better than an intelligent way to one line everything

Italo Baeza Cabrera
5 min readJan 20, 2020

When I discussed about the Manager, Andrew Brown commented about how to register the Manager on the Service Provider. It wasn’t just a simple singleton, but rather, an elegant way to replicate how Laravel itself does it with their own.

So, in this article, we are gonna go deep on the Manager and the Service Provider to keep our application expressive.

Three keys for success

Following the later article, were we create a Transport Manager for different means of transportations (drivers), the next logical step is to register it into the Service Container.

While registering a Manager as a singleton into the Service Container it’s not obligatory if you use it only once, but doing so allows the application to keep and share a single instance. This can become very useful if you use the Manager in multiple parts of your application lifecycle, like Middleware, Controllers, Events Listeners, Subscribers, and other Services — pretty much like the Authorization Manager registers itself.

If the Manager (or any class for that matter) is registered as a singleton, it won’t be instanced again, and neither a driver you already requested from it.

The registration amount to this:

There are three important statements, as you can see. We are gonna describe them one by one.

Registering the Manager

The first statement registers the Manager as a shared instance across all the application lifecycle. This means, once we call the Manager anywhere in our code using app('transport'), we will always receive the same instance.

$this->app->singleton('transport', function ($app) {
return new TransportManager($app);
});

While this is the recommended way for simple classes, you can also ask the Service Container to wrap it into a Closure and build the class for you by just issuing the Class name itself:

$this->app->singleton('transport', TransportManager::class);

Anyway, a simple Closure like before works since it avoids calling the Service Container for help.

Now that we have the manager, it would be cumbersome to call it every time just to get the driver. For that purpose, we use a second line.

Registering the Default Driver

The second line will bind the default driver in the Manager. Using this Closure, the Service Container will instance the Manager automatically if it wasn’t instanced before, and magically return to you the default driver.

$this->app->singleton('transport.driver', function ($app) {
return $app['transport']->driver();
});

This also allows us to change the default driver before retrieving it, like the Authenticate Middleware does thanks to some additional methods in the Auth Manager.

Okay, but what about Dependency Injection? We don’t want to use app('transport.driver') every time we need it, we want to inject it in the method signature. And for that, aliases are for.

Aliasing the Driver

The best way to tell the Service Container to inject your default driver into a method or class is just to alias it.

The next line tells the Service Container to consider each call to the TransportContract interface like it was done to transport.driver. That Contract exists because we always should try to bind to implementations when possible. So I made it up, but also allows any Transport abide to the interface methods.

$this->app->alias('transport.driver', TransportContract::class);

With that, you can get the default Transport Driver injected into anything the Service Container is asked to instantiate, like a Controller, without ever knowing what particular driver is being used — they’re all implementing the same Contract, which is just logical.

public function send(TransportContract $transport, Package $package)
{
$transport->send($package)->to('Ireland');
}

Bonus: Aliasing the Manager

Some classes may want to also retrieve the Manager to change the default driver to use, or do other shenanigans. In that case, you can also alias the Manager.

$this->app->alias('transport', TransportManager::class);

Word of Advice: Always prefer a Class FQN

When registering a service as a string like myservice you have to be very careful:

  • You won’t be able to use Dependency Injection when you use this way to register your service; you will need to retrieve it directly from the Service Container, like through app('service')or resolve('service').
public function send()
{
app('transport.driver')->send($package)->to('Ireland');
}
  • You may have other packages or services that may register the same name. For example, you may install Intervention, and then you register the same service name. Watch out for these packages.

The tradeoff is simplicity. You can easily remember transport instead of a fully qualified name (FQN) like SuperTransportManager::class.

Photo by Jon Tyson on Unsplash

I consider it a tradeoff because you can’t use Dependency Injection properly: the Service Container won’t be able locate the singleton you have registered as transport, and if you issue the Class name it will instance it anew every time it gets called.

If you do the same across all the your application, multiple instances will be created, and thus multiple drivers will be instanced, and that may consume more memory.

Imagine like going to by 10 apples, but buying them one by one. That’s how it feels.

To avoid this when using simple strings, you should alias it with their correct Class or Interface, which is done in the third line. Laravel does this automatically using the Application Container, sometimes with both the Class and Interface.

Personally, except on external packages, I consider it very pointless since you can just register a service by its FQN, avoiding aliasing it later altogether:

$this->app->singleton(TransportManager::class, function ($app) {
return new TransportManager($app);
});

I would do it only to disable Dependency Injection on the service itself, forcing the developer to use a global helper or a Facade, like the auth() or Auth, respectively. This can be handy when the Service registered should be only called on certain conditions, instead of injecting in unnecessarily everywhere:

public function send(Package $package)
{
if ($package->cantBeSent()) {
throw new PackageCantSendException($package);
}
// Now we can call the default transport driver using a Facade
Transport::send($package)->to('Ireland');
session()->flash('alert', trans('package.sent')); return back();
}

And that’s all folks! Happy coding.

--

--

Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com