Внедрение установщика Symfony для полиморфного сервиса

Я использую 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
  • Используйте автопроводку, чтобы я не загромождал свой файл сервисов и не смущал будущих разработчиков.

Это раздражает feature. Вы можете создать FooInterface локатор сервисов и внедрить его. Затем ребенок вытаскивал конкретную реализацию, в которой он нуждался. Это делает всех счастливыми для дополнительной строки кода. С другой стороны, вам нужно подумать о том, почему PHP делает это. Вы говорите, что хотите контракт, но на самом деле вы концептуально нарушаете контракт в своих дочерних классах.

Cerad 02.01.2023 18:32

@Cerad Я не согласен с тем, что нарушаю контракт. Я использую полиморфизм. Например, FooInterface определяет контракт о том, что есть метод getName(), возвращающий строку. В моих дочерних классах мне нужно звонить getName(). Мне все равно, какую реализацию getName() я получу, если я знаю, что метод существует и возвращает строку. Единственный способ, которым я нарушаю контракт, - это если один из моих дочерних классов зависит от поведения, не определенного в контракте, и в этом случае моя IDE будет жаловаться на неизвестный метод и т. д.

patrick3853 03.01.2023 01:23
Именованные псевдонимы, пожалуй, самый простой подход. Подпись вашего дочернего класса будет FooInterface $fooService1, где имя аргумента будет сопоставлено с конкретной реализацией FooInterface. Symfony делает это довольно часто с такими вещами, как LoggerInterface, поэтому это должно быть проверено разработчиками в будущем. Вы даже можете использовать проход компилятора для создания псевдонима на основе имени класса обслуживания.
Cerad 03.01.2023 14:54

Я бы все же сказал, что ваш первоначальный подход был нарушением. Родитель говорит, что будет внедрен FooInterface. Ребенок говорит нет, я приму FooService, который, конечно, может иметь дополнительные общедоступные методы. Это может стать проблемой, если один из ваших будущих разработчиков не осознает, что добавление методов в реализацию FooInterface было недопустимо из-за того, что делает конкретный дочерний класс. Я знаю, что это не по теме, но PHP, кажется, согласен со мной.

Cerad 03.01.2023 14:59

Кстати, вы также можете использовать атрибут Target или Autowire, чтобы указать, какой сервис внедрить, не задействуя файл services.yaml.

Cerad 12.01.2023 16:29
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
2
5
58
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Создайте родительский класс (например, FooObject) для FooInterface и FooService.

Сделайте FooInterface и FooService... расширениями этого нового класса

Определите $fooDependency как FooObject

Тогда как Symfony узнает правильную реализацию FooInterface для внедрения?

patrick3853 02.01.2023 17:14

Я не знаю, знает ли об этом Symfony, возможно, вам следует проверить свой метод, является ли он экземпляром FooService, и выдать ошибку, если нет. Надеюсь, это поможет вам.

Timuran Bicer 03.01.2023 11:04

Я хочу:

  • Принудительно использовать дочерние классы с методом установки (или каким-либо другим способом внедрить зависимость)
  • Разрешить каждому дочернему классу внедрять другую реализацию 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]

Другие вопросы по теме