Like all core Laravel features, sending mail is made easy and convenient by the clean and expressive API that it…
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.
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.
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.