Я использую Symfony 5.4 и PHP 7.4 с конфигурацией службы по умолчанию. У меня есть такой абстрактный сервис:
abstract class Parent
{
protected FooInterface $fooDependency;
public abstract function setFooDependency(FooInterface $fooDependency): void;
}
В моих дочерних классах я хочу внедрить разные реализации FooInterface для каждого дочернего элемента. Проблема в том, что PHP справедливо жалуется на несовместимость объявления дочернего метода, если тип аргумента отличается от FooInterface:
class Child extends Parent
{
public abstract function setFooDependency(FooService $fooDependency): void {}
}
Я могу решить эту проблему, не определяя setFooDependency() в абстрактном родительском элементе, но это в первую очередь противоречит цели наличия абстрактного класса (я хочу обеспечить соблюдение контракта!)
Другой обходной путь — реализовать setFooDependency() в родительском классе (вместо того, чтобы делать его абстрактным), а затем в services.yaml вручную указать аргумент для каждого дочернего класса:
services:
App\Service\Child:
calls:
- setFooDependency: [App\Service\FooService]
Но мне это не нравится, потому что любой, кто расширяет абстрактный класс, должен волшебным образом знать, что нужно пойти и добавить определение службы.
Я надеюсь, что мне не хватает простого решения? Я хочу:
FooInterface@Cerad Я не согласен с тем, что нарушаю контракт. Я использую полиморфизм. Например, FooInterface определяет контракт о том, что есть метод getName(), возвращающий строку. В моих дочерних классах мне нужно звонить getName(). Мне все равно, какую реализацию getName() я получу, если я знаю, что метод существует и возвращает строку. Единственный способ, которым я нарушаю контракт, - это если один из моих дочерних классов зависит от поведения, не определенного в контракте, и в этом случае моя IDE будет жаловаться на неизвестный метод и т. д.
FooInterface $fooService1, где имя аргумента будет сопоставлено с конкретной реализацией FooInterface. Symfony делает это довольно часто с такими вещами, как LoggerInterface, поэтому это должно быть проверено разработчиками в будущем. Вы даже можете использовать проход компилятора для создания псевдонима на основе имени класса обслуживания.
Я бы все же сказал, что ваш первоначальный подход был нарушением. Родитель говорит, что будет внедрен FooInterface. Ребенок говорит нет, я приму FooService, который, конечно, может иметь дополнительные общедоступные методы. Это может стать проблемой, если один из ваших будущих разработчиков не осознает, что добавление методов в реализацию FooInterface было недопустимо из-за того, что делает конкретный дочерний класс. Я знаю, что это не по теме, но PHP, кажется, согласен со мной.
Кстати, вы также можете использовать атрибут Target или Autowire, чтобы указать, какой сервис внедрить, не задействуя файл services.yaml.






Создайте родительский класс (например, FooObject) для FooInterface и FooService.
Сделайте FooInterface и FooService... расширениями этого нового класса
Определите $fooDependency как FooObject
Тогда как Symfony узнает правильную реализацию FooInterface для внедрения?
Я не знаю, знает ли об этом Symfony, возможно, вам следует проверить свой метод, является ли он экземпляром FooService, и выдать ошибку, если нет. Надеюсь, это поможет вам.
Я хочу:
- Принудительно использовать дочерние классы с методом установки (или каким-либо другим способом внедрить зависимость)
- Разрешить каждому дочернему классу внедрять другую реализацию FooInterface
- Используйте автопроводку, чтобы я не загромождал свой файл сервисов и не смущал будущих разработчиков.
Существуют различные методы достижения полиморфизма в PHP, но кажется, что вы сможете решить эту проблему, определив абстрактный метод для получения экземпляра FooInterface вместо метода установки. Это позволит вам внедрить конкретную реализацию FooInterface непосредственно в ваш конструктор, сохраняя неизменность.
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;
}
}
Используя этот подход, ваш родительский класс будет зависеть не от свойства, а скорее от метода, который должен реализовать каждый дочерний класс. Это обеспечит большую гибкость, удобство сопровождения, а автоматическое подключение DI не будет проблемой.
Я думаю, что проще использовать конструктор:
abstract class Parent
{
protected FooInterface $fooDependency;
publich function __construct(FooInterface $fooDependency) :void
{
$this->fooDependency = $fooDependency;
}
}
Затем вы можете определить каждую реализацию в services.yml, не беспокоясь о вызове метода:
YourNamespace\Child:
class: YourNamespace\Child
arguments: [@your_foo_implementation_alias_for_this_child]
Это раздражает
feature. Вы можете создать FooInterface локатор сервисов и внедрить его. Затем ребенок вытаскивал конкретную реализацию, в которой он нуждался. Это делает всех счастливыми для дополнительной строки кода. С другой стороны, вам нужно подумать о том, почему PHP делает это. Вы говорите, что хотите контракт, но на самом деле вы концептуально нарушаете контракт в своих дочерних классах.