It is a question I saw somewhere on Slack: Why create a manual factory, if Magento 2 is generating one for you anyway? Well ... there's various reasons why this could be something you need to do. Let's go through a couple of those reasons in this blog writing.
The essentials
Before diving into reasons why you might want to create a manual factory, let's pause for a moment on what this is about. In Magento 2, constructor-based dependency injection allows you to inject dependencies in your class, but by default all these dependencies are generated by the Magento object manager via its get
method as a singleton. As soon as you have to have multiple instances of the same classes, you will need to call upon the create
method of the object manager somehow. And a factory allows for this.
The word factory in Magento terminology gets its meaning from a broader ecosystem. It might refer to a design pattern (and there are various factory patterns actually) or it might be simply a coding concept borrowed from elsewhere (like Symfony). But it is important to point out that to a Magento developer, a factory has a specific meaning: It points to the existance of a class, ending with the word Factory
, which produces an object most commonly through a create()
method by making use of the object manager directly.
Last but not least, if a factory class is not found on the filesystem, Magento is able to create it for you.
Generated code
The following code is not a real-life coding sample of a factory that is automatically generated by the object manager, but it matches all logic though:
namespace Yireo\Example\Factory;
use Magento\Framework\ObjectManagerInterface;
class ItemFactory
{
private ObjectManagerInterface $objectManager;
public function __construct(
ObjectManagerInterface $objectManager
) {
$this->objectManager = $objectManager;
}
public function create(array $arguments = []): Item
{
return $this->objectManager->create(Item::class, $arguments);
}
}
See the definition in the earlier paragraph, as a human-readable description of what a factory is about ... by default.
If the generated factory is not good enough
The definition of what a factory is, is not set in stone. For instance, a factory might have other methods than create()
. It might not call upon the object manager but yet again other factories (so: a factory of factories). It might even offer static factory methods, so that the factory is actually not injected but statically called. Maybe these practices are less common under Magento developers, but they still fall under my interpretation of what a factor is.
Following this, I've had good reasons in the past to not let Magento generate a factory, but instead, create one myself on disk. For instance, the coding sample above uses PHP7+ type hinting (whooaa, modern!). And what if the $arguments
passed on to the create()
method need to be validated.
I've found myself creating a custom product factory multiple times: A simple create()
method would not be sufficient, because some product attributes really need to be filled in before you have a valid product object. The factory could be made more responsible by adding more methods, more dependencies, more code than the code that is generated for you.
For unit tests
Yet another strong case for creating manual factories is the issue of unit tests. Take a class that injects itself with a generated factory. You can't unit test this class, unless the factory exists (at least, to my knowledge).
Take the following service:
namespace Yireo\Example\Service;
class ExampleService
{
public function __construct(
ItemFactory $itemFactory
) {
$this->itemFactory = $itemFactory;
}
public function getFromFactory()
{
return $this->itemFactory->create();
}
}
The only job of this service is to inject itself with ItemFactory
(which does not exist but could be generated by the object manager). A testcase for this service could look like the following:
namespace Yireo\Example\Test\Unit\Service;
use PHPUnit\Framework\TestCase;
use Yireo\Example\Service\ExampleService;
use Yireo\Example\Service\ItemFactory;
class ExampleServiceTest extends TestCase
{
public function testGetFromFactory()
{
$itemFactory = $this->getMockBuilder(ItemFactory::class)
->disableOriginalConstructor()
->getMock();
$itemFactory->method('create')->willReturn('foobar');
$exampleService = new ExampleService($itemFactory);
$this->assertEquals('foobar', $exampleService->getFromFactory());
}
}
Unfortunately, running this as follows make the unit test fail:
There was 1 warning:
1) Yireo\Example\Test\Unit\Service\ExampleServiceTest::testGetFromFactory
Trying to configure method "create" which cannot be configured because it does not exist, has not been specified, is final, or is static
WARNINGS!
Tests: 1, Assertions: 0, Warnings: 1.
However, when the test is run differently by including the Magento bootstrap for unit tests (dev/tests/unit/framework/bootstrap.php
, it works:
vendor/bin/phpunit app/code/Yireo/Example/Test/Unit/Service/ExampleServiceTest.php --bootstrap=dev/tests/unit/framework/bootstrap.php
The reason for this is that the bootstrap registers a SPL autoloader Magento\Framework\TestFramework\Unit\Autoloader\GeneratedClassesAutoloader
which takes care of generated classes like factories.
Problem solved, right? Yes. But it also means that you can't run unit tests without having the Magento testing framework installed, which basically boils down that most people will run through a full Magento installation, just to be able to run unit tests. For me, it defeats the goal of having unit tests be easy to run. For me, using this autoloader is more a workaround. I'd rather stick with adding a few more lines of code that comes with a manual factory.
Don't use the object manager
Another note on this is that, in the past, unit tests could also make use of the object manager to make sure dependencies were met:
$objectManagerHelper = new \Magento\TestFramework\Helper\ObjectManager($this);
$objectManager = $objectManagerHelper->getInstance();
$exampleService = $objectManager->create(ExampleService::class);
This has been a practice for years (and you'll find many blogs recommending to write unit tests this way). But it is currently frowned upon. Unit tests are supposed to be simple, to the point and without references to the framework they are used in. Using the object manager complicates matters.
It is better to create a unit test by inserting dependencies created through the PHPUnit mock builder. This leads to a side effect though: If a class has 10 dependencies, you find yourself mocking the hell out of everything. Even worse, if your class calls upon a dependency of a dependency of a dependency the mocks are even more complex.
There are various answers to this: First of all, make sure your class is SOLID. The complexity of your mocks reflects upon the complexity of your actual code. If your mocking becomes to complex, it is a sign of your class not being SOLID.
Another way to look at it is: If a class its purpose is mainly to call upon other dependencies, a unit test will prove not that much. An integration test is much better. This is a personal opinion of mine: Don't focus upon a 100% unit test coverage and a 100% integration test coverage - just make sure there are enough tests to build confidence.
For juniors
Another reason for creating manual factories might be a bit more dubious: Theoretically, it makes it easier for newcomers (not too knowledgeable about Magento yet) to make sense of the code. I call this dubious, because if you are getting started with Magento backend development, I consider it a must to learn about the various DI mechanisms (preference, factory, proxy, type, virtual type, plugin). And when we start following this path of making Magento more readable, perhaps there's other areas to improve first.
Learn everything there is to learn about Magento 2 development with our courses, starting for backenders with our Magento 2 Backend Development I on-demand training
About the author
Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.