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:
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
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.
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`.
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:
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.