Auto-wiring for Zend ServiceManager

by Mateusz Tymek — on PHP, Zend Framework

Writing factories for zend-servicemanager can be a tedious, repetitive task. Most of factories I write follow the same pattern: pull some dependencies from the container, instantiate new object and return it. How can you avoid the repetition?

Use case

Say we have a service for processing e-mails. It consumes two dependencies: mail transport, and renderer:

class Mailer
{
    public function __construct(MailTransportInterface $transport, MailRenderer $renderer)
    {
      // ...
    }
}

Your factory will likely look like this:

class MailerFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new Mailer(
            $container->get(MailTransportInterface::class),
            $container->get(MailRenderer::class)
        );
    }
}

As you see, it is very simple: factory pulls services typehinted in constructor, and creates new instance. In my projects, usually 80%-90% of all factories are using this pattern. Clearly it is something that should be automated.

Existing solutions

To avoid re-inventing the wheel, always try to find existing solution to the problem you're facing. I looked into two:

  • zend-di - can be bridged with zend-servicemanager. However it is overcomplicated for my needs and does not support caching, making it slow.
  • php-di - looks really nice, but doesn't integrate well with zend-servicemanager - in order to use it, I would need to rewrite tons of existing code.

There are also other container implementations (like Aura.Di or Symfony DependencyInjection), that support auto-wiring, unfortunately all of them share the same problem as php-di - I cannot easily use them in my current projects.

So, no luck. I had to roll-up my sleeves and wrote my own implementation.

Blast\ReflectionFactory

Inspired by a new feature of zend-mvc, I created a general factory capable of auto-wiring PHP classes. I made a few assumptions that match my coding style:

  • no setter-injections: all required dependencies are injected in constructor
  • every service is identified in dependency container using its FQCN

I also didnt want to invent swiss army knife that does everything - for example, my solution does not work for services created with scalar configuration values (SMTP transport is good example here - you need to feed it with settings like hostname and credentials). Still, even with this limitation I can cover most of my needs. If service requires some logic to be created, it is perfectly fine to write a factory.

Usage

My library is available on Packagist - you can install it using composer:

$ composer require mtymek/blast-reflection-factory

After installation, you can start using it within your dependency configuration:

use Blast\ReflectionFactory\ReflectionFactory;

return [
    'dependencies' => [
        'factories' => [
            // use normal factory for classes that require complex instantiation 
            SmtpMailTransport::class => SmtpMailTransportFactory::class,
             
            // use ReflectionFactory for auto-wiring
            MailRenderer::class => ReflectionFactory::class,
            Mailer::class => ReflectionFactory::class,
        ],
        'aliases' => [
            MailTransportInterface::class => SmtpMailTransport::class,
        ],
    ]
];

You don't need change any of the existing code - once ReflectionFactory is installed, you can use it for new services that you write.

Caching

Auto-wiring uses PHP Reflection to read typehints of service constructor. It is slow process compared to instantiating objects directly, so ReflectionFactory allows caching the result. You can use it by pointing location of cache file, using enableCache method:

\Blast\ReflectionFactory\ReflectionFactory::enableCache(
    'data/cache/reflection-factory.cache.php'
);

If you are using Zend Expressive Skeleton Application, put above code in config/container.php file.

Conclusion

Who likes coding factories?

I'm pretty sure this little package will increase productivity of my team. Feel free to use it and give me feedback on GitHub!


comments powered by Disqus