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); } }
That is it for now. Thanks for reading!
Vince
April 6, 2021 at 7:42 pm
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.
Nick Alcala
April 6, 2021 at 9:23 pm
Thank you very much for pointing it out. I have updated the content.
I have not checked your particular issue with the config. It could be that `$this->app[‘config’][‘shops.ebay’]` did not work for you. Also did you have the config files?
https://github.com/nickalcala/laravel-factory-design-pattern-example/blob/master/config/shops.php