Home > Blockchain >  Is it possible to access a Symfony service's class name dynamically in yaml definitions (servic
Is it possible to access a Symfony service's class name dynamically in yaml definitions (servic

Time:08-11

The Problem

I have a lot of classes that are children of a parent class.

The factory that I'm using to instantiate these children takes one parameter, a string that's the class name of the child so the factory can know what to build.

The Hope

I would really like to be able to solve autowiring for all of these children in one fell swoop. I know in services.yaml I can define autowiring configuration for classes recursively via a wildcard resource e.g.:

...

Namespace\:
    resource: '..namespace/foo/*'
    # configuration for all classes in the 'foo' directory
    autowire: true
    public: true

However, for my directory of children that need to be instantiated with a factory method that accepts their class name this doesn't seem apparently possible.

What I would love to be able to do is something akin to this where "$1" is equivalent to the string that matched the wildcard like many regex-based solutions allow for (including some available through Symfony itself, like routes):

...

Domain\Children\:
    resource: '../domain/children/*'
    factory: ['Domain\Factory', 'getChild']
    arguments: 'Domain\Children\$1'

However, this understandably does not work.

I know there are some dynamic solutions in the form of "expressions", and !tagged_iterator (seen here), so I suspect there may be something out there that can do what I'm looking for because the documentation on these two examples are fairly scattered. I haven't been able to find a way to dynamically get the name of a class as part of it's service definition, hoping it exists but I just can't find it.

Another thing I tried was giving the child class a function that would instantiate it as expected, accepting a parameter of the factory class, but I couldn't find a way to define a relative function to be used in place of a factory. It seems the only way to use anything other than the class's constructor is to explicitly declare a class and function pair.

Clarifications

I am not accepting any answers that involve refactoring how these classes are being built in relation to the factory I'm referring to here. This question is not "was this design a good idea", we're well past that -- this is the design I'm stuck with.

Question Summary

Is there any way to accomplish my goal of dynamically referring to the name of the class (not with an explicit string in the yaml) when defining generic autowiring configurations?

CodePudding user response:

I don't know how to do this sort of thing using yaml but a compile pass is easy enough and probably more understandable. I developed this with a new Symfony 6 app but it should work fine on earlier versions. No changes at all to the default services.yaml file.

Start with the domain objects just to be sure we are on the same page.

namespace App\Domain;

abstract class MyParent {}
class Child1 extends MyParent {}
class Child2 extends MyParent {}
class MyFactory
{
  public function getChild(string $childClass) : MyParent
  {
    // Just ignore the 'something' for now
    return new $childClass('something');
  }
}

At this point if you run bin/console debug:container Child1 you will see there is a Child1 service but it's not using MyFactory.

Now we add the compile pass:

# src/Kernel.php

# implement CompilerPassInterface
class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    // Tag all the child classes
    protected function build(ContainerBuilder $container)
    {
        $container->registerForAutoconfiguration(MyParent::class)
            ->addTag('my.child');
    }
    public function process(ContainerBuilder $container)
    {
        $factoryReference = new Reference(MyFactory::class);

        // Grab all the child services
        $childClasses = array_keys($container->findTaggedServiceIds('my.child'));

        foreach ($childClasses as $childClass) {

            // And replace with a factory service
            $definition = new Definition($childClass);

            $definition->setFactory([$factoryReference,'getChild'])
                       ->setArgument(0,$childClass);

            $container->setDefinition($childClass,$definition);
        }
        //dump($childClasses);
    }
}

Now when we check the Child1 service we can see that it calls MyFactory::getChild like we want. I made a command to test injection:

class ChildCommand extends Command
{
    public function __construct(private Child1 $child1, private Child2 $child2)
    {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        

        $io->success('Child1 ' . get_class($this->child1));
        $io->success('Child2 ' . get_class($this->child2));

        return Command::SUCCESS;
    }
}

And it all seems to work as expected.

I was a little bit worried about constructor arguments. I'm assuming your child classes might have some and I was not sure if autowire would cause issues. So I added one:

class Child2 extends MyParent
{
    public function __construct(private string $something)
    {
      
    }
}

And once again it all seemed to work. Of course it will be up to to your factory class to inject any necessary dependencies.

Enjoy

  • Related