phpsymfonydependency-injectionsoa

Symfony setter injection for polymorphic service


I'm using Symfony 5.4 and PHP 7.4 with the default service configuration. I have an abstract service like this:

abstract class Parent
{
    protected FooInterface $fooDependency;
    public abstract function setFooDependency(FooInterface $fooDependency): void;
}

In my child classes, I want to inject different implementations of FooInterface for each child. The problem is that PHP rightly complains about the child method's declaration not being compatible if the argument's type is anything other than FooInterface:

class Child extends Parent
{
    public abstract function setFooDependency(FooService $fooDependency): void {}
}

I can solve this by not defining setFooDependency() in the abstract parent, but that kind of defeats the purpose of having the abstract class in the first place (I want to enforce a contract!)

Another workaround is to implement setFooDependency() in the parent class (instead of making it abstract), then in services.yaml manually specify the argument for each child class:

services:
    App\Service\Child:
        calls:
            - setFooDependency: [App\Service\FooService]

But I'm not fond of this, because anyone extending the abstract class has to magically know to go and add the service definition.

I'm hoping I'm missing an easy solution? What I want is to:


Solution

  • What I want is to:

    • Enforce child classes having a setter method (or some other way to inject the dependency)
    • Allow each child class to inject a different implementation of FooInterface
    • Use autowiring so I don't clutter my services file or confuse future devs

    There are various methods to achieve polymorphism in PHP, but it seems that you may be able to resolve this issue by defining an abstract method to retrieve the FooInterface instance instead of a setter method. This would enable you to inject a concrete FooInterface implementation directly into your constructor while maintaining immutability.

    interface FooInterface
    {
        public function getName(): string;
    }
    
    class ConcreteFoo implements FooInterface
    {
        public function getName(): string
        {
            return 'a string';
        }
    }
    
    abstract class Father
    {
        public function doSomething(): void
        {
            $name = $this->getFoo()->getName();
            
            // ...
        }
    
        abstract protected function getFoo(): FooInterface;
    }
    
    class Child extends Father
    {
        private ConcreteFoo $foo;
        
        public function __construct(ConcreteFoo $foo) 
        {
            $this->foo = $foo;
        }
        
        protected function getFoo(): ConcreteFoo
        {
            return $this->foo;
        }
    }
    

    By using this approach, your parent class will not depend on a property, but rather on a method that every child class must implement. This will allow for greater flexibility, maintainability, and the DI autowiring won't be a problem.