Skip to main content

Replacing service dependencies with proxies

11 Oct 2014
Tags: zf2 proxy doctrine

When using the Zend Framework 2 service manager, it is possible to create shared services that will be loaded only once. In some situations however, it is very hard to switch the already injected dependencies in this service. You could mark the service as unshared even if this is often unnecessary. Another solution is to wrap the service with a proxy object and use the proxy instead of the service.

Real-life Scenario

In an application with multiple tenants, there is one database per tenant. When browsing the application the tenant's database connection is always known. This connection doesn't ever change when browsing the application. Therefor the Doctrine DocumentManager is injected and the services are marked as shared.

On the server, there are some long-running processes to handle async commands. These processes run globally and are not dependant on a specific tenant. This means that the processes need to switch the DocumentManager based on the tenant they are working for at the moment.

Off course, the registered services have to work with the current DocumentManager, even if they are marked as shared.

Delegators

One of the cool features of ZF2 are service delegators. These delegators make it possible to attach or wrap custom functionality to an instance. In this case we will use the delegator to wrap the actual service in a proxy. The instance of the service will still be created in the service manager like before, but instead of returning this instance we will return a proxy object.

Proxies

In this post, I already talked a lot about using proxies. But what are those proxies?

The short answer: Proxies are objects that serve as a gateway to the actual object.

The long answer: There are many different types of proxies. Here you can find a good overview of all proxy patterns in PHP.

In this specific case, a Virtual Proxy is the right proxy to use. It extends the actual base class and has exactly the same API. An over-simplified proxy of a Doctrine DocumentManager could look like this:

<?php

class DocumentManagerProxy
    extends DocumentManager
{

    protected $instance;

    public function __construct(DocumentManager $instance)
    {
        $this->instance = $instance;
    }
    
    public function find($className, $id)
    {
        return $this->instance->find($className, $id);
    }
    
    // ... All other methods of the DocumentManager ...

}

Because this kind of objects will result in a lot of effort in maintaining, it is better to use a library that automatically generates the proxy objects. One of those libraries is ProxyManager by Ocramius.

Putting it all together

ServiceManager configuration

<?php

return [
    'factories' => [
        'documentmanager' => new \DoctrineMongoODMModule\Service\DocumentManagerFactory('manager_key'),
        'Application\DocumentManagerProxyDelegator' => 'Application\Factory\DocumentManagerProxyDelegatorFactory',
        'Application\DynamicDocumentManager' => 'Application\Factory\DynamicDocumentManagerFactory',
    ],
    'delegators' => [
        'documentmanager' => ['Application\DocumentManagerProxyDelegator'],
    ]
];

Delegator

The delegator is responsible for initializing the actual DocumentManager and creating the proxy object. Another service is added that is responsible for maintaining and loading the different DocumentManagers. Make sure that the proxy initializer returns false, so that the proxy is initialized with the current DocumentManager on every method call.

<?php

namespace Application;

use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class DocumentManagerProxyDelegator implements DelegatorFactoryInterface
{

    /**
     * @var LazyLoadingValueHolderFactory
     */
    protected $proxyFactory;

    /**
     * @var DynamicDocumentManager
     */
    protected $dynamicDocumentManager;

    /**
     * @param $dynamicDocumentManager
     * @param $proxyFactory
     */
    public function __construct($dynamicDocumentManager, $proxyFactory)
    {
        $this->dynamicDocumentManager = $dynamicDocumentManager;
        $this->proxyFactory = $proxyFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
    {
        $dynamicDocumentManager = $this->dynamicDocumentManager;
        $currentDocumentManager = $callback();
        $dynamicDocumentManager->setCurrentManager($currentDocumentManager);

        $proxy = $this->proxyFactory->createProxy(
            'Doctrine\ODM\MongoDB\DocumentManager',
            function (& $wrappedObject) use ($dynamicDocumentManager) {
                $wrappedObject = $dynamicDocumentManager->getCurrentManager();
                return false;
            }
        );

        return $proxy;
    }

}

Delegator Factory

The delegator factory creates an instance of the factory. For this example It is very straight forward. If you are planning to use it in production, you might add some extra caching configuration to the proxy factory. This configuration can be based on `Zend\ServiceManager\Proxy\LazyServiceFactoryFactory`.

<?php

namespace Application;

use Application\DocumentManagerProxyDelegator;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class DocumentManagerProxyDelegatorFactory implements FactoryInterface
{

    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $dynamicDocumentManager = $serviceLocator->get('Application\DynamicDocumentManager');
        $proxyFactory = new LazyLoadingValueHolderFactory();
        return new DocumentManagerProxyDelegator($dynamicDocumentManager, $proxyFactory);
    }
    
}

Dynamic DocumentManager

The dynamic DocumentManager is responsible for loading and managing the DocumentManagers. While creating an instance for the DocumentManager, the real instance is injected in the DynamicDocumentManager. An implementation might look like this:

<?php

interface DynamicDocumentManagerInterface
{

    public function getCurrentManager();
    public function setCurrentManager(DocumentManager $documentManager);
    public function loadForTenant(Tenant $tenant);

}

The `setCurrentManager()` is called by the delegator to set the current manager.

The `getCurrentManager()` is called while initializing the proxy. This way, the current manager will always be used when using the `documentmanager` in a shared service.

Before executing a command in the long-running process, the `loadForTenant()` method is being called. This method will close the old manager and reset the service manager keys for all Doctrine services. Finally it will recreate the `documentmanager`, which will trigger the `setCurrentManager()` method again.

Final thoughts

This technique might look a little bit `hacky`, but is actually a very neat trick to make it easier to change instances on the fly.

You don't have to mark all services that rely on the DocumentManager as unshared. In the codebase, you can still use the actual instance type in the annotations.

You might think of some other good use-cases to implement this powerful technique yourself.

whois VeeWee

Selfie

Hi there!

Glad you made it to my blog. Please feel free to take a look around. You will find some interesting stuff, mostly about web development and PHP.

Still can't get enough of me? Quick! Take a look at my Speakerdeck, Twitter, PHPC.Social, or Github account.