26 Apr 2020
cat: Laravel, PHP
2 Comments

Laravel Factory Design Pattern Example

Introduction

In your Laravel project, imagine you have objects that mostly have the same methods or features and you want to dynamically create those objects, Factory Design Pattern helps you with that. Laravel uses this pattern when calling methods such as app('cache')->store('redis') in driver-based components. For this tutorial we will do same and create a factory to create objects that pull products from Amazon and Ebay. The Amazon and Ebay implementation will just be dummies to make the tutorial simpler.

If you want a quick reference into how to create your own driver-based components, open vendor\laravel\framework\src\Illuminate\Cache\CacheManager.php.

Sample Interface and Implementations

First we want the factory class to return the same kind of object that is why we need an interface.

app\Services\Shop\IShopService.php

namespace App\Services\Shop;

interface IShopService
{
    public function getProducts(): array;
}

Then we can write our sample implementations which will be 1 for Ebay and 1 for Amazon. Of course you can add more.

app\Services\Shop\EbayShopService.php

namespace App\Services\Shop;

class EbayShopService implements IShopService
{

    private $config;

    public function __construct($config)
    {
        dump("Ebay config was set in constructor...");
        $this->config = $config;
        dump($this->config);
    }

    public function getProducts(): array
    {
        return [
            'Ebay Product Sample #1',
            'Ebay Product Sample #2',
            'Ebay Product Sample #3',
        ];
    }
}

app\Services\Shop\AmazonShopService.php

namespace App\Services\Shop;

class AmazonShopService implements IShopService
{

    private $config;

    public function getProducts(): array
    {
        return [
            'Amazon Product Sample #1',
            'Amazon Product Sample #2',
            'Amazon Product Sample #3',
        ];
    }

    public function setConfig($config)
    {
        dump("Amazon config was set in a method...");
        $this->config = $config;
        dump($this->config);
    }
}

Creating the Factory

We can create an interface for the Factory so we can bind if with the implementation later. Then we can inject that interface whenever you can.

app\Managers\Shop\IShopManager.php

namespace App\Manager\Shop;

use App\Services\Shop\IShopService;

interface IShopManager
{
    public function make($name): IShopService;
}

app\Managers\Shop\ShopManager.php

namespace App\Manager\Shop;

use App\Services\Shop\AmazonShopService;
use App\Services\Shop\EbayShopService;
use App\Services\Shop\IShopService;
use Illuminate\Support\Arr;

class ShopManager implements IShopManager
{

    private $shops = [];

    private $app;

    public function __construct($app)
    {
        $this->app = $app;
    }

    public function make($name): IShopService
    {
        $service = Arr::get($this->shops, $name);

        // No need to create the service every time
        if ($service) {
            return $service;
        }

        $createMethod = 'create' . ucfirst($name) . 'ShopService';
        if (!method_exists($this, $createMethod)) {
            throw new \Exception("Shop $name is not supported");
        }

        $service = $this->{$createMethod}();

        $this->shops[$name] = $service;

        return $service;
    }

    private function createEbayShopService(): EbayShopService
    {
        dump("Creating EbayShopService...");
        $config = $this->app['config']['shops.ebay'];
        $service = new EbayShopService($config);
        // Do the necessary configuration to use the Ebay service
        return $service;
    }

    private function createAmazonShopService(): AmazonShopService
    {
        dump("Creating AmazonShopService...");
        $service = new AmazonShopService();
        $config = $this->app['config']['shops.amazon'];
        $service->setConfig($config);
        // Do the necessary configuration to use the Amazon service
        return $service;
    }
}

So in ShopManager‘s make method we checked in the shops array if the particular object was already created and just return that. Next we have a naming convention for the method that creates the actual objects and call it based on the name provided.

Note: In the constructor, we passed the $app which is the Laravel application. This is because we don’t have access to the config class at that point and can’t use the app('config') easily. We will get this exception instead Illuminate\Contracts\Container\BindingResolutionException: Target class [config] does not exist.

Registering the Factory to the Container

We can add this to AppServiceProvider but of course you can create a separate service provider.

app\Providers\AppServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(\App\Manager\Shop\IShopManager::class, function ($app) {
            return new \App\Manager\Shop\ShopManager($app);
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }
}

Testing it out

To test this easily we will use a unit test.

tests\Unit\ShopManagerTest.php

namespace Tests\Unit;

use App\Manager\Shop\IShopManager;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_can_use_ebay_service()
    {
        $factory = app(IShopManager::class);
        $service = $factory->make('ebay');
        $products = $service->getProducts();
        dump($products);
        self::assertEquals([
            'Ebay Product Sample #1',
            'Ebay Product Sample #2',
            'Ebay Product Sample #3',
        ], $products);
    }

    public function test_can_use_amazon_service()
    {
        $factory = app(IShopManager::class);
        $service = $factory->make('amazon');
        $products = $service->getProducts();
        dump($products);
        self::assertEquals([
            'Amazon Product Sample #1',
            'Amazon Product Sample #2',
            'Amazon Product Sample #3',
        ], $products);
    }
}

Laravel Factory Design Pattern Example

That is it for now. Thanks for reading!

References


  1. Thank you for providing this. The “dump($this->config);” on EbayShopService.php file returns NULL, same on AmazonShopService.php.
    I don’t see why “$this->stores[$name] = $service;” uses $this->stores on ShopManager.php file. Shouldn’t that be $this->shops. I did change it to $this->shops but the “dump($this->config);” still returned NULL.

Post A Comment