Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decorate all services that implement the same interface by default?

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?

like image 261
yivi Avatar asked Jan 25 '23 23:01

yivi


1 Answers

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());
    }
}
like image 133
msg Avatar answered Jan 30 '23 14:01

msg