Marco Pivetta (Ocramius)

Delegator Factories in Zend Framework 2

Last year, I worked on a feature for ZF2 called "Delegator Service Factories" , which was included in Zend Framework 2.2.0.

It seems to me that many ZF2 developers either don't fully understand the feature, or do not know it.

This article analyzes the feature in depth and tries to explain what these do, and why you may need them.


The Problem

While working with Zend\ServiceManager, we often find ourselves in the need of overriding services provided by third-party modules that we use.

Let's make a practical example and assume that we are using a DbLoggingModule. provided by a friendly open-source developer.

The developer of DbLoggingModule provided us with an awesome service called "DbLoggingModule\Logger", which is created by a service factory DbLoggingModule\Factory\LoggerFactory:

<?php

class LoggerFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $sm)
    {
        $config = $sm->get('Config');
        $db     = new DB($config['db_logging']['dsn']);

        // more db configuration here

        $logger = new Logger($db);

        $logger->addFilter(new ErrorFilter());

        // more logger configuration here

        return $logger;
    }
}

This is awesome and clean, but then we want to be able to log all errors by removing the pre-configured filters, or we want to add a formatter to our logger to add contextual information to the logged messages.

This becomes tricky, since we now have to define our own factory for that logger:

<?php

class MyLoggerFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $sl)
    {
        $config = $sl->get('Config');
        $db     = new DB($config['db_logging']['dsn']);

        // more db configuration here

        $logger = new Logger($db);

        // removing this: we don't filters
        // $logger->addFilter(new ErrorFilter());

        // this will add some context from the HTTP globals
        // ("eeew", but useful for our logged messages)
        $logger->addFormatter(new HttpRequestContextFormatter());

        // more logger configuration here

        return $logger;
    }
}

This is fine, but our MyLoggerFactory duplicates a lot of code from LoggerFactory, and we now have to also strictly monitor any updates by the original developer of DbLoggingModule.

Code duplication is a huge problem, especially when we're localizing code from external dependencies.

We can mitigate this issue by using an initializer instead:

<?php

class MyLoggerInitializer implements InitializerInterface
{
    public function initialize($instance, ServiceLocatorInterface $sl)
    {
        if ($instance instanceof Logger) {
            $logger->clearFilters();
            $logger->addFormatter(new HttpRequestContextFormatter());
        }
    }
}

This is a much cleaner approach, but there are major disadvantages as well:

  • This initializer is being called once per each instantiated service, and that can lead to hundreds (seriously!) of useless method calls per request, and that for a single object that we wanted to change.
  • All of our Logger instances are going to be affected by the change, and that is a big problem if we have more than one logger in the application.

That's pretty much technical debt that we are building up. We are being lazy, and we will pay for that if we go down this route.

The solution for this particular problem is using "delegator factories".


What are delegator factories?

A delegator factory is pretty much a wrapper around a real factory: it allows us to either replace the real service with a "delegate", or interact with an object produced by a factory before it is returned by the Zend\ServiceManager.

In pseudo-code, a delegator-factory is doing following:

service = delegatorFactory(factory());

This is the interface for a delegator factory:

<?php

namespace Zend\ServiceManager;

interface DelegatorFactoryInterface
{
    public function createDelegatorWithName(
        ServiceLocatorInterface $serviceLocator,
        $name,
        $requestedName,
        $callback
    );
}

Delegator Factories applied to the Logger problem

This is how we would use it to modify our "DbLoggingModule\Logger" service:

<?php

class LoggerDelegatorFactory implements DelegatorFactoryInterface
{
    public function createDelegatorWithName(
        ServiceLocatorInterface $serviceLocator,
        $name,
        $requestedName,
        $callback
    ) {
        $logger = $callback();

        $logger->clearFilters();
        $logger->addFormatter(new HttpRequestContextFormatter());

        return $logger;
    }
}

Note: We are not using the first 3 parameters, which may be useful in different contexts (for example, when configuration is needed).

We then add it to our service manager configuration to instruct the Zend\ServiceManager that we want our delegator factory to be used whenever the service "DbLoggingModule\Logger" is requested:

<?php
return [
    'delegators' => [
        'DbLoggingModule\Logger' => [
            'LoggerDelegatorFactory',
            // can add more of these delegator factories here
        ],
    ],
];

This will make the Zend\ServiceManager call the the LoggerDelegatorFactory#createDelegatorWithName() method whenever the service "DbLoggingModule\Logger" is instantiated, regardless if it is built via invokable, factory, peering service manager or abstract factory.

Hint: You can define more of these delegator service factories for a single service, which allow you to override service instantiation logic in different modules and multiple times, leading to a very fine-grained configuration flexibility.

Hint: Assuming that you want to completely replace the "DbLoggingModule\Logger" with your own custom implementation depending on context, you could also completely avoid using the provided $callback. This way, the original service won't even be instantiated.

On naming

The name delegator factory is actually something I'm not happy with, since they are actually just wrappers around an existing factory.

Warning: Since I'm the first guy to shout out at Laravel Facades, I want to make it clear that the naming "Delegator Factory" is wrong, that it was my mistake, and I will likely fix it for ZendFramework 3.x.

I initially designed these factories to solve a different problem, which is Lazy Services.

In the context of lazy services and decorators, the instance returned by these factories is indeed a "delegate", therefore I came up with the name "delegator factory".

Only later on I realized that these factories are actually much more powerful than what I originally designed them for, therefore the "delegator factory" name became inadequate.


Conclusions

Delegator factories are vital to any ZF2 hacker. Any developer working with Zend\ServiceManager should also be familiar with this functionality and its usage.

Delegator factories are not a new concept. For instance, Pimple introduced this concept a while ago and (in a limited form): it is called "extending a service".

The entire functionality was introduced by accident while I was working on an implementation of Lazy Services, and they surely need a rename. This will not happen in Zend Framework 2.x.

I hope this helps you with your day-to-day hacking around your ZF2 apps!

Tags: zf2, zendframework2, oop, php