Reusing factories in Zend ServiceManager

I think it is doubtless that modern PHP embraces SOLID principles, and therefore, dependency injection.

That's why every modern PHP application needs a dependency injection container to deal with it.

There are several options out there, depending on the way you like to work. Every container has a slightly different approach.

My choice is zend-servicemanager, it is the one that better suits me.

Other containers rely on auto wiring and auto discovery in order to know which dependencies need to be injected on every service, and I think that leads to errors when an application grows.

I like zend-servicemanager because it is explicit, and you are always in control of what's done, without loosing flexibility in the process.

However, I have to recognize that it comes with a prize.

Factories everywhere

Since this container expects you to define factories for every service, you usually end up writing, testing and maintaining a lot of factories that doesn't add value to the application.

That's why it is so important to properly reuse factories when possible, not only because you will have to maintain less classes, but because the ServiceManager will instantiate less objects at runtime when it can reuse a factory.

This article is born because of the question of a reader of this blog, which asked me what did I mean with the "redundancy mitigated by reusing same factory" sentence, in one of my Zend Expressive articles.

I created a gist with some of the approaches to reuse factories, but I think it deserves a blog post, so here it is.

Shared dependencies

The simplest situation to reuse a factory is when you have more than one service with the same dependencies.

This situation can be solved with an abstract factory, but you can also use a concrete factory, which is more efficient.

Let's imagine you are using the zend-db package to deal with persistence, and you have created one TableGateway class for every table in your database.

All of the TableGateways depend on a Zend\DB\Adapter\AdapterInterface to be injected on them, so creating a different factory for every one of them would be a waste of time.

Instead, you just need to create a factory like this:

<?php
declare(strict_types=1);

namespace App;

use Interop\Container\ContainerInterface;
use Zend\DB\Adapter\AdapterInterface;

class TableGatewayFactory
{
    public function __invoke(ContainerInterface $container, string $requestedName)
    {
        $adapter = $container->get(AdapterInterface::class);
        return new $requestedName($adapter);
    }
}

Then, register your table gateways using the FQCN:

<?php
declare(strict_types=1);

namespace App;

return [

    'service_manager' => [
        'factories' => [
            UserTableGateway::class => TableGatewayFactory::class,
            ArticleTableGateway::class => TableGatewayFactory::class,
            FooTableGateway::class => TableGatewayFactory::class,
            // ...
        ],
    ],

];

If you prefer using an abstract factory, just do this:

<?php
declare(strict_types=1);

namespace App;

use Interop\Container\ContainerInterface;
use Zend\DB\Adapter\AdapterInterface;
use Zend\DB\TableGateway\TableGateway;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;

class TableGatewayAbstractFactory implements AbstractFactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName)
    {
        return is_subclass_of($requestedName, TableGateway::class);
    }

    public function __invoke(ContainerInterface $container, $requestedName)
    {
        $adapter = $container->get(AdapterInterface::class);
        return new $requestedName($adapter);
    }
}
<?php
declare(strict_types=1);

namespace App;

return [

    'service_manager' => [
        'abstract_factories' => [
            TableGatewayAbstractFactory::class,
        ],
    ],

];

Using ConfigAbstractFactory

The zend-servicemanager v3.2 introduced a built-in factory that can inject dependencies on services based on configuration.

This is probably one of the better ways to reuse factories (indeed, you won't have to write any factory, since this one is included in the package).

In 80% of the cases (if not more), a factory basically consists on grabbing some dependencies from the container, and creating a new instance of an object where those dependencies are injected. In those cases the ConfigAbstractFactory is perfect.

When using this factory, you just need to define a configuration block where you define the service names on which every other service depends.

For example, if we have this code base:

<?php
declare(strict_types=1);

namespace App;

class OtherService
{
    public function __construct()
    {
        // No dependencies
    }
}

class BazService
{
    public function __construct()
    {
        // No dependencies
    }
}

class BarService
{
    public function __construct(BazService $baz)
    {
        // Depends on BazService
    }
}

class FooService
{
    public function __construct(BarService $baz, OtherService $other)
    {
        // Depends both on BarService and OtherService
    }
}

We just need to define a configuration like this:

<?php
declare(strict_types=1);

namespace App;

use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;

return [
    'service_manager' => [
        'factories' => [
            FooService::class => ConfigAbstractFactory::class,
            BarService::class => ConfigAbstractFactory::class,
            BazService::class => InvokableFactory::class,
            OtherService::class => InvokableFactory::class,
        ],
    ],

    ConfigAbstractFactory::class => [
        BarService::class => [BazService::class],
        FooService::class => [BarService::class, OtherService::class],
    ],
];

Then, the ConfigAbstractFactory will look for all the services on which requested service depends, and inject them into it.

Also, the package includes a binary that can be used to generate the ConfigAbstractFactory config for a service, so you won't even need to write that.

This factory can be registered as an abstract factory too (indeed, it is an abstract factory), but as mentioned above, it is less efficient, and I prefer this approach.

You have an in-detail documentation of this factory here: https://docs.zendframework.com/zend-servicemanager/config-abstract-factory/

Using ReflectionBasedAbstractFactory

This factory works similarly to the previous one, but instead of discovering the dependencies to inject based on a configuration array, it uses reflection on requested service.

This has the small advantage that you don't have to specify the dependencies for every service. However, reflection is very inefficient, so this factory is not suited for production, but only for prototyping purposes.

Also, you need to register services with the same type used in the requested service constructor, so you can't type hint to an interface and then inject a service which is registered with an implementation name, or a string which is not even a class name.

Apart from that, registration is the same as in previous factories:

<?php
declare(strict_types=1);

namespace App;

use Zend\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;

return [

    'service_manager' => [
        'factories' => [
            FooService::class => ReflectionBasedAbstractFactory::class,
            BarService::class => ReflectionBasedAbstractFactory::class,
            BazService::class => InvokableFactory::class,
            OtherService::class => InvokableFactory::class,
        ],
    ],

];

The extended documentation for this factory can be found here: https://zendframework.github.io/zend-servicemanager/reflection-abstract-factory/

Using acelaya/zsm-annotated-services package

Some time ago (when the ConfigAbstractFactory didn't exist yet), I created the acelaya/zsm-annotated-services package, which provides one factory that can discover the list of dependencies of a service based on an @Inject annotation in its constructor, instead of using a configuration array.

The first example could be redeclared like this:

<?php
declare(strict_types=1);

namespace App;

use Acelaya\ZsmAnnotatedServices\Annotation\Inject;

class OtherService
{
    public function __construct()
    {
        // No dependencies
    }
}

class BazService
{
    public function __construct()
    {
        // No dependencies
    }
}

class BarService
{
    /**
     * @Inject({BazService::class}) 
     */
    public function __construct(BazService $baz)
    {
        // Depends on BazService
    }
}

class FooService
{
    /**
     * @Inject({BarService::class, OtherService::class}) 
     */
    public function __construct(BarService $baz, OtherService $other)
    {
        // Depends both on BarService and OtherService
    }
}

And then, you just need to map the services to the AnnotatedFactory (select the right one, depending on the ServiceManager version)

<?php
declare(strict_types=1);

namespace App;

use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Zend\ServiceManager\Factory\InvokableFactory;

return [

    'service_manager' => [
        'factories' => [
            FooService::class => AnnotatedFactory::class,
            BarService::class => AnnotatedFactory::class,
            BazService::class => InvokableFactory::class,
            OtherService::class => InvokableFactory::class,

            // You will probably need to define some cache adapter in production
            AnnotatedFactory::CACHE_SERVICE => SomeCacheFactory::class,
        ],
    ],

];

The only drawback of this factory, is that you need to define a cache adapter in production, because processing annotations is very slow (it uses reflection too).

Here you have the complete documentation: https://github.com/acelaya/zsm-annotated-services.

However, if I were you, I would choose the ConfigAbstractFactory over this one, because using annotations couples the configuration with the service.

Conclusion

The problem of defining several factories is usually the main argument I hear against using this component, but as you can see, there are several approaches to deal with it, without loosing control over your code base or depending on black magic.

There are probably other options that I've missed here, so if you know any other approach, just comment this post and I will gladly add it.