To avoid instantiating services that might not be used, we use lazy services that get instantiated only when their proxy is interacted with. Symfony comes with a proxy generator, but it is kind of too much for what I need, and worst of all, it doesn't support final classes. Tired of writing handcrafted proxies, I finally rolled up my sleeves to write my own generator.
https://github.com/olvlvl/symfony-dependency-injection-proxy
Update-20181201
Since
symfony/dependency-injection
v4.2 it is possible to specify the interface that should be implemented by the proxy. The article was updated to demonstrate how to use that new feature.
Symfony's proxies
Symfony uses ocramius/proxy-manager to generate its proxies. It is a great solution, and it can proxy everything and its dog, but it is not exactly tailored for dependency injection concerns. Also, it produces a large amount of code, like 10Kb for a class with one property and one method. Check this comparison to get an idea. Besides, it is not working with final classes, which all of my classes implementing an interface are.
Handcrafted proxies
I couldn't bring myself to use Symfony's proxy generator. I had no need for most of the code that
was generated, I didn't want to remove final
from my classes, and I had the sneaking suspicious it
was impacting the performance of my application. So instead, I was writing my proxies by hand, and
of course their tests too.
The following code is a proxy I wrote for a logger implementing LoggerInterface
. You will
immediately notice there are a couple of sad things going on. The proxy has knowledge of the whole
container and requires the service identifier. I could use a service locator that would only hold
that one service, but frankly, at this point, why bother? It does the job, but none of this is
great.
<?php
namespace HelloFresh\MenuService\Infrastructure\Logger;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;
final class LoggerProxy implements LoggerInterface
{
use LoggerTrait;
/**
* @var ContainerInterface
*/
private $container;
/**
* @var string
*/
private $id;
/**
* @var LoggerInterface
*/
private $service;
public function __construct(ContainerInterface $container, string $id)
{
$this->container = $container;
$this->id = $id;
}
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
$this->getService()->log($level, $message, $context);
}
private function getService(): LoggerInterface
{
return $this->service ?: $this->service = $this->container->get($this->id);
}
}
The following code is the configuration required to wire the service and its proxy. You will notice that the actual service needs to be public since the service will be instantiated from outside the container.
services:
# …
logger:
class: Psr\Log\LoggerInterface
factory: "logger_builder:build"
arguments:
- "%env(HF_SYSLOG_HOST)%"
- "%env(HF_SYSLOG_PORT)%"
public: true
Psr\Log\LoggerInterface:
class: HelloFresh\MenuService\Infrastructure\Logger\LoggerProxy
arguments:
$service: logger
The one thing I liked was the decorator-like vibe of my proxies. Because they aren't extending the class, I can proxy final classes too, which most of my classes are. I wanted to keep that but get rid of the container, the service id, and keep private services private… And so began my quest for the tiniest proxies, and, to my despair, the quest for documentation (spoiler alert: there is none). I guess that is what Sundays are for.
Designing my own proxy generator
Compared to ocramius/proxy-manager, which can proxy you to the moon, my proxies are more like decorators, implementing the same interface as their target service. Also, because they leverage anonymous classes they're way simpler to generate and happily fit in the same method as their factory.
Consider the following class and interface:
<?php
interface SampleInterface
{
public function getValue(): string;
}
final class Sample implements SampleInterface
{
/**
* @var string
*/
private $value;
public function __construct(string $value)
{
$this->value = $value;
}
/**
* @inheritdoc
*/
public function getValue(): string
{
return $this->value;
}
}
A proxy for Sample
would look something like this:
<?php
// …
protected function getSampleInterfaceService($lazyLoad = true)
{
if ($lazyLoad) {
return $this->services['SampleInterface'] = new class(
function () {
return $this->getSampleInterfaceService(false);
}
) implements \SampleInterface
{
private $factory, $service;
public function __construct(callable $factory)
{
$this->factory = $factory;
}
public function getValue(): string
{
return ($this->service ?: $this->service = ($this->factory)())->getValue();
}
};
}
return new \Sample('aValue');
}
// …
The dumped container is now ~2600 bytes, and I can keep final
on my class. Compared to the ~10500
bytes of Symfony's, it is a nice reduction in size and complexity.
Building the container
The container is built the same way as with symfony/proxy-manager-bridge:
<?php
use olvlvl\SymfonyDependencyInjectionProxy\FactoryRenderer;
use olvlvl\SymfonyDependencyInjectionProxy\InterfaceResolver\BasicInterfaceResolver;
use olvlvl\SymfonyDependencyInjectionProxy\MethodRenderer;
use olvlvl\SymfonyDependencyInjectionProxy\ProxyDumper;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
/* @var ContainerBuilder $builder */
$builder->compile();
$dumper = new PhpDumper($builder);
$dumper->setProxyDumper(new ProxyDumper());
What if my lazy service implements multiple interfaces?
The basic interface resolver will have a hard time figuring out which interface to implement if a
service implements several. For instance, if a service was an instance of ArrayObject
the following
exception would be raised:
Don't know which interface to choose from for ArrayObject: IteratorAggregate, Traversable, ArrayAccess, Serializable, Countable.
Since symfony/dependency-injection
v4.2 it is possible to specify which interface should be
implemented by the proxy using the lazy
keyword:
services:
ArrayObject:
lazy: ArrayAccess
For older versions, we can help by decorating the basic interface resolver with a map and specifying
which interface to implement for which class. The following example demonstrates how we hint that
proxies to ArrayObject
services should implement ArrayAccess
:
<?php
/* @var PhpDumper $dumper */
$dumper->setProxyDumper(new ProxyDumper(
new MapInterfaceResolver(new BasicInterfaceResolver(), [
ArrayObject::class => ArrayAccess::class,
])
));
Simpler service definitions
Now it is super simple to make my services lazy. Remember the definition for my logger?
services:
# …
logger:
class: Psr\Log\LoggerInterface
factory: "logger_builder:build"
arguments:
- "%env(HF_SYSLOG_HOST)%"
- "%env(HF_SYSLOG_PORT)%"
public: true
Psr\Log\LoggerInterface:
class: HelloFresh\MenuService\Infrastructure\Logger\LoggerProxy
arguments:
$service: logger
It now looks like this:
services:
# …
Psr\Log\LoggerInterface:
factory: "logger_builder:build"
arguments:
- "%env(HF_SYSLOG_HOST)%"
- "%env(HF_SYSLOG_PORT)%"
lazy: true
Conclusion
I'm happy with my solution: it is straightforward to use, it generates super tiny proxies, and it can proxy final classes. I was able to get rid of my handcrafted proxies without changing anything to my applications. I even switched the lazy mode on a couple of services I was too lazy (pun!) to write proxies for. With any luck, it could suit your application as well. Let me know :)