How to use the Manager pattern in Laravel

Hi there Titans (ooh used it finally)!

For the piece, I'll be showing you how you can use the Manager pattern in Laravel. I'll assume you already know the basics of the framework and the underlying language (PHP). Right? Cool.

Every now and then in Laravel, you write a piece of code that looks like this.

Class::driver('some-driver')->getStuffDone();

In which Class is a Facade, and you can call different drivers on the same facade to do the same thing, using different underlying implementations.

Here are some examples that you most often use.

The Storage Facade

Using this Facade you can determine where you want your files to be stored, by passing different drivers.

You can pass the local driver for storing items locally. i.e.

Storage::disk('local')->put($path, $contents)

Or you can decide to store your files in s3 and do

Storage::disk('s3')->put($path, $contents)

While you have switched between different drivers (or disks) the put method is consistent and does the same thing irrespective of the object it is acting on.

The different disks you can switch to are stored in a config file filesystems.php , this tells the different underlying implementations, which configurations to use (more on this later).

Another example.

The Log Facade

This is used for logging (obviously), and you may use this to log into different platforms; you can log to file, stderr, stdout, slack, bug tracking platforms, APM and monitoring tools, etc.

And the syntax is usually similar to

Log::driver('stdout')->info("Logging bugs to stdout", [$stackTrace]);

And, the different drivers can be found in the logging.php , and of course, you could specify others.

I believe this paints the picture.

Now, the main advantage of using this pattern is the ability to switch drivers at runtime without the need to make conditional checks.

A driver is an implementation of a contract.

A key component of this pattern is a contract. This is how you ensure that the different drivers maintain a consistent method signature.

Great, Laravel uses this, why would I need to use it? What's the use case for me?

Sample use case:

Let's say you want to have a price checker implementation, that can get you the price of a commodity from different platforms. I'd use Amazon, Alibaba, and eBay as examples of the platforms we want to get the prices of a commodity.

Let's start!

Step 1: Create the Interface

In this step, we'll create the contract that will bind all the implementing classes later on.

Let's call it the CommodityPriceInterface and we can define it like this.

<?php

namespace App\Interfaces;

use App\Commodity;

interface CommodityPriceInterface
{
    /**
    * Get price of the commodity
    * @param Commodity $commodity
    * @return mixed
    */
    public function getPrice(Commodity $commodity): mixed;

    /**
    * Create new price record
    * @param array $data
    * @return mixed
    */
    public function createNewPriceRecord(array $data): mixed;

    /**
    * Update price record
    * @param array $data
    * @return mixed
    */
    public function updatePriceRecord(array $data): mixed;
}

We have created an interface above that will be the contract for all our implements.

Now, let's implement the interface.

Step 2: Create the Implementations

The Amazon Implementation

<?php 

namespace App\Drivers;

class AmazonCommodityPriceDriver implements CommodityPriceInterface
{
    public function __construct(public AmazonImaginarySDK $amazonSDK) {

    }

    public function getPrice(Commodity $commodity): mixed {
        return $this->amazonSDK->getPriceOfAnItem($commodity->amazon_id);
    }

    public function createNewPriceRecord(array $data): mixed {
        return AmazonPriceTable::create($data);
    }

    public function updatePriceRecord(array $data): mixed {
        return AmazonPriceTable::update($data);
    }
}

The Alibaba Implementation

Much like the Amazon one, we'll have something like

<?php 

namespace App\Drivers;

class AlibabaCommodityPriceDriver implements CommodityPriceInterface
{
    public function __construct(public AlibabaImaginarySDK $alibabaSDK) {

    }

    public function getPrice(Commodity $commodity): mixed {
        return $this->alibabaSDK->getPriceOfAnItem($commodity->alibaba_id);
    }

    public function createNewPriceRecord(array $data): mixed {
        return AlibabaPriceTable::create($data);
    }

    public function updatePriceRecord(array $data): mixed {
        return AlibabaPriceTable::update($data);
    }
}

The eBay Implementation

<?php 

namespace App\Drivers;

class EBayCommodityPriceDriver implements CommodityPriceInterface
{
    public function __construct(public EBayImaginarySDK $eBaySDK) {

    }

    public function getPrice(Commodity $commodity): mixed {
        return $this->eBaySDK->getPriceOfAnItem($commodity->ebay_id);
    }

    public function createNewPriceRecord(array $data): mixed {
        return EbayPriceTable::create($data);
    }

    public function updatePriceRecord(array $data): mixed {
        return EbayPriceTable::update($data);
    }
}

Step 3: Now, we'll create the Manager!

(I'll let the code do the talking)

<?php

namespace App\Managers;

use Illuminate\Support\Manager;
use App\Drivers\AmazonCommodityPriceDriver;
use App\Drivers\AlibabaCommodityPriceDriver;
use App\Drivers\EbayCommodityPriceDriver;

class CommodityPriceManager extends Manager
{

    public function getDefaultDriver(): string
    {
        return config('commodityprice.default') ?? 'amazon';
    }

    public function createAmazonDriver(): CommodityPriceInterface
    {
        return new AmazonCommodityPriceDriver();
    }

    public function createAlibabaDriver(): CommodityPriceInterface
    {
        return new AlibabaCommodityPriceDriver();
    }

    public function createEbayDriver(): CommodityPriceInterface
    {
        return new EbayCommodityPriceDriver();
    }
}

Step 4: Create our config

// config/commodityprice.php
<?php 

'default' => env('COMMODITY_PRICE_DEFAULT')

'drivers' => [
    'amazon' => [
        'api_key' => env('AMAZON_API_KEY')
    ],
    'alibaba' => [
        'api_key' => env('ALIBABA_API_KEY')
    ],
    'ebay' => [
        'api_key' => env('EBAY_API_KEY')
    ]
]

Step 5: Register the manager in the AppServiceProvider

<?php

namespace App\Providers;

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

Step 6: Create the Facade

<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

/**
* @method static CommodityPriceInterface getDefaultdriver()
* @method static CommodityPriceInterface driver(string $name)
* @method static CommodityPriceManager extend(string $driver, \Closure $callback)
* @method static mixed getPrice(Commodity $commodity)
* @method static mixed createNewPriceRecord(array $data)
* @method static mixed updatePriceRecord(array $data)
*/
class CommodityFacade extends Facade
{
    /**
    * Get the registered name of the component.
    * @return string
    *
    * @throws \RuntimeException
    */
    protected static function getFacadeAccessor()
    {
        return 'commodity_price';
    }
}

Now we are ready to use our implementation!

Example usage:

<?php

namespace App\Services;

use App\Commodity;

class SomeServiceThatUsesCommodityPrice
{
    public function priceChecker(Commodity $commodity): mixed {
        return [
            'amazon_price' => CommodityFacade::driver('amazon')->getPrice($commodity),
            'alibaba_price' => CommodityFacade::driver('alibaba')->getPrice($commodity),
            'ebay_price' => CommodityFacade::driver('ebay')->getPrice($commodity),
        ]
    }
}

With this, we could add/remove more drivers as we want. And use any driver at any point in the runtime.

I hope you find this useful.

I'll catch you in the next one.

O dabo.