Building Driver-based Components in Laravel

Let’s talk about building driver-based components in laravel.

Orobo Ozoka
ITNEXT

--

Componentization is a great way to build extensible and reliable software systems. It allows us to build large systems that are composed of decoupled, independent and reusable components. It gives us a plug-and-play approach to building software systems.

Laravel as a framework is richly composed of reusable components — some of which are third-party Symfony components — that are all well-defined and pieced together to make up the system.

Components

Most modern software systems are built by assembling small, self-contained, and reusable entities that provide specific services and functionality to the system. A software component is, essentially, a small unit, with usually well-defined interfaces that form the basis of composition for a larger system. It encapsulates a set of related functions (or data) into a reusable unit.

Driver-based Components

Components are usually entities that enforce separation of concerns in a software system. They are modular and are responsible for delivering specific services to an application, say, for example, a Session component that handles states in a web application. What’s interesting is, you can build components in a manner that allows them to deliver their service in different ways while still providing the same contract it promises. This is the driver-based approach to designing components.

At the very core, you design the component with extensibility in mind, in a way that allows its default behavior to be replaced by objects that implement the component’s contract.

A driver is a specific implementation of a component’s contract to a software system. It provides an interface to an underlying infrastructure upon which the component’s service is built.

This idea of drivers and driver-based components is built into laravel and it’s supported out-of-the-box by the framework. This is the aspect of the framework we want to explore and see how we can use this pattern in our applications to build driver-based components.

Enter Managers!

Managers

When we build our driver-based components, we need a way to manage them. We want to be able to create multiple predefined drivers or even create them at a later time during the application’s lifecycle. We want to be able to request instances of a particular driver and also have a fallback driver where calls are proxied into, for when we don’t specify a driver. This is the job of a Manager.

The manager is an entity that manages the creation of driver-based components. It is responsible for creating specific driver implementation based on an application’s configuration.

The manager is designed around the idea that components can have multiple drivers (instances of a component that are implemented differently). Using a manager, a component can define the logic that is needed to create drivers it supports. The manager acts as a hub for created and custom drivers of a component and it’s the gateway into the component.

As previously mentioned, laravel ships with support for managers and we want to leverage that to create our driver-based components. Let’s get into more details about how the manager works.

The Manager Class

Laravel provides an abstract Manager class in the Support namespace (Illuminate\Support\Manager). This class defines useful methods to help us manage our drivers. To get started, you extend the manager class and define driver creation methods in the subclass (your component’s manager class).

use Illuminate\Support\Manager;class FooManager extends Manager
{
//
}

Creating Drivers

Of course, creating a driver-based component requires us to be able to create drivers. The manager class defines a createDriver($driver) method that does exactly what it says on the tin, create a new driver instance. The method accepts a single argument; the name of the driver to create. It makes the assumption that the extending class has defined creational methods that create the drivers. These creational methods should have the following signature:

create[Drivername]Driver()

where Drivername is the name of the driver after it has been studly-cased.

The driver creation methods you define in your manager class should return an instance of the driver.

Obtaining a Driver

It’s like ordering an Uber, you get an instance of the manager and call the driver($driver = null) method on it. The base manager provides this method and it accepts a single optional argument; the name of the driver whose instance you want to obtain. The manager then goes and make the driver for you by calling the appropriate driver creation method you have defined in your manager class. If you don’t pass the name of the driver you want to obtain to the driver($driver = null) method, it returns an instance of the default driver.

The Fallback Driver

The manager is an abstract class and declares an abstract getDefaultDriver() method that must be defined by the extending manager class. This method should return the name of the default driver that should be used by the component when no driver is specified. This fallback driver should act as the primary.

Extending the Component

You can add custom drivers that were not predefined by a driver-based component by calling the extend() method on the manager. This method provides you with a way to register custom driver creators using a Closure. When you request a driver from the manager, it checks if a custom driver creator exists for that driver and calls the custom creator. The Closure registered as the custom creator receives an instance of \Illuminate\Foundation\Application when it’s being called.

protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}

These custom drivers can override predefined drivers with the same name in the manager if they haven’t already been created.

If you would like to see the complete implementation of the base Manager class, check it out on Github. (Laravel 5.7 as at the time of publishing this article)

Alright, so enough talk, let’s see managers in action by building a simple driver-based SMS component in Laravel.

Photo by Joanna Kosinska on Unsplash

The SMS Component

We want to build a straightforward SMS component with multiple drivers. Out of the box, the component will support three drivers: A Nexmo Driver, a Twilio Driver, and a Null Driver. As we’ll see later in this article, we can also extend the component and create custom drivers for it.

Our component will live within the App\Components\Sms namespace of our application. First, let’s create our component’s ServiceProvider:

This registers our sms component as a singleton in the service container and returns an instance of the component’s Manager (App\Components\Sms\SmsManager). Let’s quickly register our Service Provider with Laravel in config/app.php:

'providers' => [
// Other service providers...

App\Providers\SmsServiceProvider::class,
],

Next, let’s go ahead and define the manager:

The first thing to notice is how our manager class extends Laravel’s Manager (Illuminate\Support\Manager). This is the first step to creating driver-based components in laravel. The base manager class defines the logic to aid in the creation and managing of our drivers. Because it’s an abstract class and declares a getDefaultDriver() method that must be implemented, we’ve defined that method in our manager class and returned the default driver:

/**
*
* Get the default SMS driver name.
*
*
@return string
*/
public function getDefaultDriver()
{
return $this->app['config']['sms.default'] ?? 'null';
}

You’ll notice that the config is coming from a config file (config/sms.php) that we’ve defined to hold information about our component. It’s a pretty straight forward file that contains credentials for each driver as well as the default driver to use. If a default driver isn’t set, we’ll fall back to the NullDriver.

To conveniently choose which driver to use, the manager class defines a driver($name) method that returns an instance of the specified driver. We’ve created a convenience method for our component named channel($name) that calls the base driver($name) method and passes the name of the driver we want to obtain to it.

/**
* Get a driver instance.
*
*
@param string|null $name
*
@return mixed
*/
public function channel($name = null)
{
return $this->driver($name);
}

Our drivers live in App\Components\Sms\Drivers namespace. To create our drivers, we defined 3 creational methods that create each of the drivers that are supported out of the box by the component. All drivers extends a base Driver class that implements our component’s contract (App\Components\Sms\Contracts\SMS). This contract declares a send method that must be implemented by all drivers of the component. This is the component’s service contract to the system and it promises to be able to send SMS.

<?phpnamespace App\Components\Sms\Contracts;interface SMS
{
/**
* Send the given message to the given recipient.
*
*
@return mixed
*/
public function send();
}

Let’s take a look at the NexmoDriver to see how our component works internally:

The driver implements the send method and delivers a message using the Nexmo PHP Client.

Once we have our component all set up, that is, registering a facade for our component as SMS, setting up the config file, and installing our dependencies, we can quickly use the component by obtaining an instance of SmsManager and calling the send method on it.

SMS::to($phoneNumber)
->content('Building driver-based components in Laravel')
->send();

The to($phoneNumber) and content($message) methods are defined by the base Driver class that is extended by all drivers in the component.

Here, we are not specifying a driver to use so it defaults to our Nexmo driver because that is what we made the default driver in our component. To specify a driver, we can either call the channel($name) method or the base driver($name) method.

SMS::channel('twilio')
->to($phoneNumber)
->content('Using twilio driver to send SMS')
->send();

And there it is, we’ve successfully created a driver-based component in laravel. I’ve added the source to GitHub if you’re interested in seeing the complete implementation.

Now that we’ve created the component, we can add more drivers by extending the component and adding custom driver creators. To do this, we obtain an instance of SmsManager and call the extend method on it. But first, we’ll create a driver that implements the component’s contract:

<?phpuse App\Components\Sms\Contracts\SMS;class FooSmsDriver implements SmsContract
{
protected $someDependency;
public function __construct($dependency)
{
$this->someDependency = $dependency;
}
public function send()
{
// Define send logic
}
}

Then, we’ll call the extend method and provide the custom driver creation logic for it.

SMS::extend('foo', function ($app) {
return new FooSmsDriver($dependency);
});

Once we’ve registered the custom driver creator, we can now use it as we would other out-of-the-box drivers supported by the component.

SMS::channel('foo')
->send();

Using a test, we can verify that the component is able to use multiple drivers including custom created drivers.

Conclusion

Laravel makes it painless to create driver-based components using a Manager class. I should quickly point out that it’s not set in stone to always use this Manager class when building Driver-based components, you can build your own manager that handles driver creation and management for your components.

I hope this article was helpful, thanks for reading.

--

--