Implement hot swapping service-drivers in your Laravel app.

We've all wondered at some point or another how does Laravel implement swapping of services like the database driver just by editing a config value? I've touched a little bit on this topic in this post but it was more of how to use the feature to change database connections programmatically, in today's post we're implementing the feature from scratch for swapping SMS providers.

We're going to leverage one of Laravel's most powerful features; the dreaded service container! bear with me and hopefully by the end of the post all will be clear, let's warm-up by a brief explanation of the container.

What is the Service Container in the first place?

If you're confused by the container as we all were, let me tell you that it's a fancy name for a class that you give it some kind of an identifier (usually, the fully qualified class name, FQCN) and it will give you back in instance of it; resolving its dependencies recursively along the way. An example to clear things a bit.

Imagine you have a class for an SMS gateway, and this class requires an instance of Guzzle, normally you'd want to pass an instance of Guzzle to the constructor to create an object of SmsGateway. yeah, the Container would do that for you, for example

// App\SmsGateway
class SmsGateway
{
public function __construct(GuzzleHttp\Client $client) { ... }
}
 
// Traditionally
$smsGateway = new App\SmsGateway(new GuzzleHttp\Client);
 
// Using the Container
$smsGateway = app()->make('App\SmsGateway');

As you see we don't need to give it the instance of Guzzle as it's a simple object requiring a nullable array of configuration, the Container is smart enough to inject any simple dependencies automatically and recursively, so if Guzzle depends on some other object it would inject that too. btw, by simple I mean it doesn't depend on some value that Laravel doesn't know how to get, like an API key (You can tell Laravel how to do it, but that's out of scope for now).

The other trick you need to know is that we can bind strings to classes.
WTF?
Yeah, we can teach Laravel that when I ask the container for an instance of the string "app" give me an instance of Illuminate\Foundation\Application, if that's not awesome I don't know what is!
Spoiler: that's exactly how Facades work 🙄. If you want to learn more about how Facades work, Caleb Porzio did an awesome video explaining that here.

Let's get to implementation.

First Step: Create a Facade

Creating a Facade in Laravel is a matter of creating a class that extends \Illuminate\Support\Facades\Facade and implements one method, getFacadeAccessor() that would return the string Laravel will use to resolve the instance, as we said, usually it's the FQCN.

<?php
 
namespace App\Facades;
 
use Illuminate\Support\Facades\Facade;
 
class Messenger extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return \App\Messenger::class;
}
}

guess what? we have a fully functioning Facade! So, if App\Messenger has a method send() all of the below will work.

// Using the "new" keyword
(new App\Messenger)->send();
 
// Using the container
app()->make('App\Messenger')->send();
 
// Using the Facade
App\Facades\Messenger::send();

additionally, if you want to use it like Laravel's facades and do \Messenger::send(), you'll need to add 'Messenger' => App\Facades\Messenger::class, to the aliasases array in config\app.php;

Second Step: Hire a Manager

Great, now we need a place to hold the logic that will determine which class to use based on the config file.
Laravel uses the concept of managers to handle this, so let's stick to that.

<?php
 
namespace App\Managers;
 
class MessengerManager
{
 
}

now we need to edit our Facade to point to the manager class instead, we'll resolve the right class there and pass all calls to the resolved instance

protected static function getFacadeAccessor()
{
return \App\Managers\MessengerManager::class;
}

let's imagine that we have a driver() method that will give us an instance of the resolved class, we can pass all method calls that are not on the manager class down to the resolved instance, this can be easily achieved by the magic method __call()

public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

the driver() method just gets the class identifier from the config (this could be the FQCN of the service, or maybe a string that is bound to a class in one of your service providers) and let the container resolve that. super simple, very testable!

here we're also handling if the user wants to use a driver other than the default one for a single call

public function driver($name = null)
{
// if the user is not specifing a driver name, we'll use the default.
$driver = $this->getConfig($name ?? $this->getDefaultDriver());
 
if (empty($driver['class']))) {
throw new \Exception('Class of driver is not defined.');
}
 
return $this->app->make($driver['class'], ['driver' => $driver]);
}

we then need two methods, getConfig() and getDefaultDriver(), getConfig() will grab a driver name and query the config repository for its array.

protected function getConfig($name)
{
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["sms.drivers.{$name}"];
}
 
return ['driver' => 'null'];
}
 
public function getDefaultDriver()
{
return $this->app['config']['sms.default'];
}

We can also treat ourselves with a setDefaultDriver() method for those times when we need to change the default without passing the non-default driver name every time.

public function setDefaultDriver($name)
{
$this->app['config']['sms.default'] = $name;
}

That's pretty much the meat of it, you'll need to adapt the manager class to your needs, for example, Laravel takes this a few steps further like caching resolved Queue connections.

you can take inspirations from these classes:
- Illuminate\Database\DatabaseManager
- Illuminate\Queue\QueueManager
- Illuminate\Filesystem\FilesystemManager

Hope that unveiled a little of the magic behind facades and drivers!