background

February 8, 2022

Why create manual factories in Magento 2?

Yireo Blog Post

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.

Posted on February 8, 2022

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

Read more

About the author

Author Jisse Reitsma

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.

Sponsor Yireo

Looking for a training in-house?

Let's get to it!

We schrijven niet te commerciële dingen, we richten ons op de technologie (waar we dol op zijn) en we komen regelmatig met innovatieve oplossingen. Via onze nieuwsbrief kun je op de hoogte blijven van al deze coolness. Inschrijven kost maar een paar seconden.

Do not miss out on what we say

This will be the most interesting spam you have ever read

We schrijven niet te commerciële dingen, we richten ons op de technologie (waar we dol op zijn) en we komen regelmatig met innovatieve oplossingen. Via onze nieuwsbrief kun je op de hoogte blijven van al deze coolness. Inschrijven kost maar een paar seconden.