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:
FooInterface
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.