I have a growing number of service classes that share a common interface (let's say BarService
and BazService
, that implement FooInterface
).
All of these need to be decorated with the same decorator. Reading the docs, I know that I can do:
services:
App\BarDecorator:
# overrides the App\BarService service
decorates: App\BarService
Since I have to use the same decorator for different services I guess I would need to do:
services:
bar_service_decorator:
class: App\BarDecorator
# overrides the App\BarService service
decorates: App\BarService
baz_service_decorator:
class: App\BarDecorator
# overrides the App\BazService service
decorates: App\BazService
Problem is: this gets repetitive, quickly. And every time a new implementation of FooInterface
is created, another set needs to be added to the configuration.
How can I declare that I want to decorate all services that implement FooInterface
automatically, without having to declare each one individually?
A compiler pass allows to modify the container programmatically, to alter service definitions or add new ones.
First you'll need a way to locate all implementations of FooInterface
. You can do this with the help of autoconfigure:
services:
_instanceof:
App\FooInterface:
tags: ['app.bar_decorated']
Then you'll need to create the compiler pass that collects all FooServices
and creates a new decorated definition:
// src/DependencyInjection/Compiler/FooInterfaceDecoratorPass.php
namespace App\DependencyInjection\Compiler;
use App\BarDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FooInterfaceDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(BarDecorator::class)) {
// If the decorator isn't registered in the container you could register it here
return;
}
$taggedServices = $container->findTaggedServiceIds('app.bar_decorated');
foreach ($taggedServices as $id => $tags) {
// skip the decorator, we do it's not self-decorated
if ($id === BarDecorator::class) {
continue;
}
$decoratedServiceId = $this->generateAliasName($id);
// Add the new decorated service.
$container->register($decoratedServiceId, BarDecorator::class)
->setDecoratedService($id)
->setPublic(true)
->setAutowired(true);
}
}
/**
* Generate a snake_case service name from the service class name
*/
private function generateAliasName($serviceName)
{
if (false !== strpos($serviceName, '\\')) {
$parts = explode('\\', $serviceName);
$className = end($parts);
$alias = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($className)));
} else {
$alias = $serviceName;
}
return $alias . '_decorator';
}
}
Finally, register the compiler pass in the kernel:
// src/Kernel.php
use App\DependencyInjection\Compiler\FooInterfaceDecoratorPass;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FooInterfaceDecoratorPass());
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With