paint-brush
How to Extend Laravel With Driver-Based Servicesby@valerio-barbera

How to Extend Laravel With Driver-Based Services

by Inspector.devSeptember 12th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, I talk about a Laravel internal feature not mentioned in the official documentation called “Driver Manager” It can completely change the way you design and develop your application solving critical architectural bottlenecks. It was widely used by the creators of the framework to abstract common services allowing you to interact with different types of technologies for each of these services. Log, Cache, Session, and Log services are perfect examples of how the Driver Manager works. I’ll show you how to develop your own ‘driver-based” component and how to bind it to the IoC container.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Extend Laravel With Driver-Based Services
Inspector.dev HackerNoon profile picture


Hi, I’m Valerio, software engineer, and CTO at Inspector.


In this article, I talk about a Laravel internal feature not mentioned in the official documentation called “Driver Manager.” It can completely change the way you design and develop your application solving critical architectural bottlenecks, allowing you to build large systems built around decoupled, independent, and reusable components.


It was widely used by the creators of the framework to abstract common services allowing you to interact with different types of technologies for each of these services.


Think about Log (you can log to files, syslog, papertrail, slack, etc.), Cache (you can cache your data into files, redis, memecached, etc.), Session (you can store PHP sessions using files, databases, etc.), and others general-purpose components.


How the Driver Manager Works in the Laravel Framework

As mentioned above, Laravel already provides many components built using the “driver” system, and you can access them by Facades:


\Log::debug('message');

\Cache::get('key');

\Session::get('key');


When You Use These Facades, How Does Laravel Know Which Implementation (the Driver) Should be Used?


Each service has its own configuration file. In the case of the Cache service, it is in the config/cache.php configuration file:


/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
| Supported: "apc", "array", "database", "file",
|            "memcached", "redis", "dynamodb"
|
*/

'default' => env('CACHE_DRIVER', 'file'),


Changing the value of the default driver from “file” to “database,” Laravel will automatically use the database implementation instead of files to store and retrieve cached items.


We can also switch from one driver to another at runtime if necessary:


\Cache::driver('file')->put('key', 'value');


Obviously, it works only if you have properly configured the driver in the configuration file.

Below I’ll show you how to develop your own “driver-based” component and how to bind it to the IoC container to be reachable by a Facade class.


When to Develop Driver-Based Services?


A driver-based service is the right choice when the same utility can be provided by more than one technology.


Thanks to the drivers, you can develop a concrete implementation for each underlying technology and switch among them, changing a simple configuration parameter or even more simply changing an environment variable.


This strategy allows you to use the file as your cache storage in your development environment and Redis in production simply by setting a different value for the CACHE_DRIVER environment variable.


# DEV environment - Set file as default cache storage
CACHE_DRIVER=file

# PROD environment - Set Redis as default cache storage
CACHE_DRIVER=redis


Log, Cache, or Session services are perfect examples. Log, Cache, and Session are functional needs, regardless of the underlying technology you want to use.


Every developer needs to cache temporary information out of the database to speed up the performance, but a cache service can be provided by many different technologies such as Redis, Memcached, files, etc.


Regardless of the technology you use, you always need to store a value in the cache:


\Cache::put('key', 'value');


And retrieve these values from the cache:


$value = \Cache::get('key');


To better understand how this mechanism works, I’ll show a real-life example of building our internal Firewall service.


Based on the definition we discussed above, a Firewall is a functional need that can be addressed using several types of systems: Cloudflare, fail2ban, Google Cloud Armor, AWS hosted firewall, etc.


Implement the Firewall Component


First, we need to define the general Interface of a Firewall that all drivers must implement.

Looking at the purpose of a Firewall, we generically should be able to “deny” IP addresses to reach our infrastructure or “allow” them to send traffic to it.


Here is the Firewall interface:


namespace App\Firewall\Contracts;


interface FirewallInterface
{
    /**
     * Allow web traffic from the give ip addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function allow(array $ips);

    /**
     * Deny web traffic from the given IP addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function deny(array $ips);
}


All drivers will have to implement this interface, so when we develop the “driver manager,” we can skip from one driver to another, being sure that the application continues to work.


The first demo implementation could be a simple logger to write the list of the given IP addresses in the log file. We call it LogFirewallDriver:


namespace App\Firewall\Drivers;


use App\Firewall\Contracts\FirewallInterface;
use Psr\Log\LoggerInterface;

class LogFirewallDriver implements FirewallInterface
{
    /**
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * LogFirewallDriver constructor.
     *
     * @param LoggerInterface|null $logger
     */
    public function __construct(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    /**
     * Allow web traffic from the give ip addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function allow(array $ips)
    {
        if ($this->logger) {
            $this->logger->debug('Allow traffic from: ' . implode(', ', $ips));
        }
    }

    /**
     * Deny web traffic from the given IP addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function deny(array $ips)
    {
        if ($this->logger) {
            $this->logger->debug('Deny traffic from: ' . implode(', ', $ips));
        }
    }
}


How to Implement the Manager class


When we build our driver-based components, we need a way to manage them. We want to be able to create several predefined drivers or even create them at a later time during the application’s lifecycle.


As seen at the beginning of the article, we want to request instances of a particular driver at runtime and have a fallback driver where calls are proxied into, for when we don’t specify a driver.


This is the job of the \Illuminate\Support\Manager class.

Laravel provides this abstract Manager class in the Support namespace (Illuminate\Support\Manager) that contains some built-in functionality to help us manage the driver system.


To get started, you need to extend this class and define your own driver creation methods like the createLogDriver() method:


namespace App\Firewall;


use Illuminate\Support\Manager;

class FirewallManager extends Manager
{
    /**
     * Get the default driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return config('firewall.default') ?? 'log';
    }

    /**
     * Get an instance of the log driver.
     *
     * @return LogFirewallDriver
     */
    public function createLogDriver(): FirewallInterface
    {
        return new LogFirewall(
            $this->container['log']->channel(config('firewall.drivers.log.channel'))
        );
    }
}


The driver creation method should respect the format 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 interface.


The base manager class defines several built-in logics to aid in the creation and managing of our drivers. Because it’s an abstract class and declares a getDefaultDriver() method, you’ve to implement this method returning the default driver’s name.


How to Bind the FirewallManager Component to the IoC Container


To access the Firewall component within the application, you need to register the FirewallManager class into Laravel’s service container. Add the code below in your AppServiceProvider:


namespace App\Providers;


use App\Firewall\FirewallManager;

class AppServiceProvider extend ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('firewall', function ($app) {
            return new FirewallManager($app);
        });
    }
}


We also create the Firewall Facade to access the binded service in a convenient way:


namespace App\Firewall\Facades;


/**
 * @method static FirewallInterface getDefaultDriver()
 * @method static FirewallInterface driver(string $name)
 * @method static FirewallManager extend(string $driver, \Closure $callback)
 * @method static mixed allow(array $ips)
 * @method static mixed deny(array $ips)
 */
class Firewall extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     *
     * @throws \RuntimeException
     */
    protected static function getFacadeAccessor()
    {
        return 'firewall';
    }
}


Finally create the config\firewall.php configuration file to store the specific configuration options for each driver:


return [
    /*
    |--------------------------------------------------------------------------
    | Default Firewall Driver
    |--------------------------------------------------------------------------
    |
    | This option defines the default firewall driver that gets used when try to
    | allow or deny traffic for some IPs addresses. The name specified in this option should match
    | one of the driver defined in the "drivers" configuration array.
    |
    */

    'default' => env('FIREWALL_DRIVER', 'log'),

    /*
    |--------------------------------------------------------------------------
    | Configuration options for each driver
    |--------------------------------------------------------------------------
    |
    | Here you may configure the firewall drivers for Inspector. Out of
    | the box, Inspector is able to allow and deny traffic using the
    | firewalls below.
    |
    */

    'drivers' => [
        'log' => [
            'channel' => 'daily'
        ]
    ]
];


How to Use the Firewall Component


Once we have configured our component, that is, registering a facade as Firewall and setting up the config file, we can easily get an instance of the FirewallManager and access the driver functionalities:


\Firewall::deny(['127.0.0.1', '127.0.0.2']);


By default, it uses the log driver, so you should see the log entry:


[2021-08-19 14:37:55] local.DEBUG: Deny traffic from: 127.0.0.1, 127.0.0.2


Add a New Driver


With the FirewallManager in place, we can now easily develop new firewall implementations to interact with other systems. Just as an example, I’ll show you how to implement and add the “Cloudflare” driver to interact with the Cloudflare firewall without touch the application’s code.

As seen for the implementation of the LogFireallDriver we need to create the CloudflareFirewallDriver implemeting the general firewall interface:


namespace App\Firewall\Drivers;


use App\Firewall\Contracts\FirewallInterface;

class CloudflareFirewallDriver implements FirewallInterface
{
    /**
     * Http client to interact with Cloudflare API.
     *
     * @var \Guzzle\Client $client
     */
    protected $client;

    /**
     * CloudflareFirewallDriver constructor.
     *
     * @param string $zoneId
     */
    public function __construct(string $zoneId)
    {
        $this->client = new \Guzzle\Client('https://api.cloudflare.com/client/v4/zones/' . $zoneId)
    }

    /**
     * Allow web traffic from the given ip addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function allow(array $ips)
    {
        // Call Cloudflare API to allow traffic from the given IP addresses.
        $this->client->put('filters', ...);
    }

    /**
     * Deny web traffic from the given IP addresses.
     *
     * @param array $ips
     * @return mixed
     */
    public function deny(array $ips)
    {
        // Call Cloudflare API to deny traffic from the given IP addresses.
        $this->client->put('filters', ...);
    }
}


As you can see in the constructor method, you need to provide the “zoneId” to properly build the Cloudflare API endpoint. You can add a new entry in the config/firewall.php configuration file for this new driver:


return [
    /*
    |--------------------------------------------------------------------------
    | Default Firewall Driver
    |--------------------------------------------------------------------------
    |
    | This option defines the default firewall driver that gets used when try to
    | allow or deny traffic for some IPs addresses. The name specified in this option should match
    | one of the driver defined in the "drivers" configuration array.
    |
    */

    'default' => env('FIREWALL_DRIVER', 'log'),

    /*
    |--------------------------------------------------------------------------
    | Configuration options for each driver
    |--------------------------------------------------------------------------
    |
    | Here you may configure the firewall drivers for Inspector. Out of
    | the box, Inspector is able to allow and deny traffic using the
    | firewalls below.
    |
    */

    'drivers' => [
        'log' => [
            'channel' => 'daily'
        ],

        'cloudflare' => [
            'zone' => 'xxx'
        ]
    ]
];


We also must make the FirewallManager aware of this new driver. You can add the new createCloudflareDriver() method to define the creation logic:


namespace App\Firewall;


use Illuminate\Support\Manager;

class FirewallManager extends Manager
{
    /**
     * Get the default driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return config('firewall.default') ?? 'log';
    }

    /**
     * Get an instance of the log driver.
     *
     * @return LogFirewallDriver
     */
    public function createLogDriver(): FirewallInterface
    {
        return new LogFirewall(
            $this->container['log']->channel(config('firewall.drivers.log.channel'))
        );
    }


    /**
     * Get an instance of the Cloudlfare driver.
     *
     * @return CloudflareFirewallDriver
     */
    public function createCloudflareDriver(): FirewallInterface
    {
        return new CloudflareFirewallDriver(
            config('firewall.drivers.cloudflare.zone'))
        );
    }
}


Now you are free to switch to this new driver or configure it as the default driver in your environment file:


\Firewall::driver('cloudflare')->deny(['127.0.0.1', '127.0.0.2']);


Conclusion


Laravel makes it painless to create driver-based components using the Manager class. I learned about it by exploring the framework by myself. It is a habit that I advise you to adopt too because it always offers new opportunities to learn and improve your development skills.


New to Inspector?


Create a monitoring environment specifically designed for software developers avoiding any server or infrastructure configuration that many developers hate to deal with.

Thanks to Inspector, you will never have the need to install things at the server level or make complex configurations in your cloud infrastructure.


Inspector works with a lightweight software library that you can install in your application like any other dependencies. In the case of Laravel, you have our official Laravel package at your disposal. Developers are not always comfortable installing and configuring software at the server level because these installations are out of the software development lifecycle or are even managed by external teams.


Visit our website for more details: https://inspector.dev/laravel/