Использование инъекции зависимостей в веб-приложении PHP

В последнее время я много читаю об инъекции зависимостей, и теоретически это имеет большой смысл. Идея иметь простой тест, отдельные классы ответственности просто звучит разумно. Тем не менее, я борюсь с тем, как далеко его принять.

Я работаю над веб-приложением PHP прямо сейчас, у которого есть компонент CMS и сильно управляется базой данных. В CMS вы можете создавать страницы, а сами страницы создаются с использованием различных виджетов. Виджеты могут быть прямым HTML (из редактора WYSIWYG), или могут быть немного сложнее, например, в фотогалерее. И это важная часть, виджет фотогалереи зависит от других моделей db, таких как PhotoGallery и PhotoGalleryImages, чтобы нормально функционировать.

Согласно Miško Hevery, я не должен передавать ссылки на зависимости через разные уровни моего приложения. Вместо этого каждый класс должен просто "запрашивать" любые зависимости, которые он требует. Используя его пример, вот как это выглядит:

$db = new Database()
$repo = new UserRepository($db);
$page = new LoginPage($repo);

Пока я вижу этот рабочий режим для уровней обслуживания моего приложения, я действительно не понимаю, как это будет работать с моими объектами/объектами ценности. Мне кажется, что эта модель требует от меня экземпляров всех моих моделей на самом высоком уровне моего приложения. Однако как я могу создать экземпляр всех моих виджетов на этом уровне, когда мне нужен мой класс, чтобы рассказать мне, какие виджеты мне нужно создать? И не зная, какие виджеты я создаю, как я могу узнать, какие зависимости виджета нужно создавать и вводить?

В теории DI имеет для меня большой смысл. Однако фактически реализация этого шаблона в моем новом веб-приложении оказывается более сложной. Есть ли что-то, что мне не хватает в отношении DI и IoC? Благодарю!

Обновление: ответ на Tandu

Большое спасибо за ответ Танду. Я думаю, что DI не может быть на самом деле моей проблемой здесь, потому что я думаю, что я очень четко понимаю, что это такое, будь то инсталляция конструктора или сеттера, или это делается с использованием контейнера или бедных людей. То, что я действительно думаю, происходит потому, что DI бросает вызов мне переосмыслить, как я сейчас программирую. Я думаю, что мой проект ООП - это проблема. Чтобы проиллюстрировать это, вот как я обычно запрограммировал бы эту проблему:

class Page
{
    public $id;
    public $name;

    public function widgets()
    {
        // Perform SQL query to select all widgets
        // for this page from the database, and then
        // return them as Widget objects.
    }
}

interface Widget
{
    public $id;
    public $page_id;
    public $type;

    public function widgetize();
}

class PhotoGallery_Widget implements Widget
{
    public $id;
    public $page_id;
    public $type;

    public function widgetize()
    {
        $gallery = new Photogallery();

        foreach($gallery->images())
        {
            // Create gallery HTML code
        }
    }
}

// Finally, to create the page, I would:
$page = new Page(1);
foreach($page->widgets() as $widget)
{
    $widget->widgetize();
}

Ясно, что здесь есть некоторые проблемы. Модель Page зависит от моделей Widget. Модель PhotoGallery_Widget зависит от модели PhotoGallery, и даже более того модель PhotoGallery зависит от модели PhotoGalleryImages. Дело в том, что развитие этого пути хорошо сработало для меня. По мере того как я становлюсь все глубже и глубже в слоях моего приложения, каждый слой берет на себя ответственность за создание требуемых объектов. Введение DI бросает важный ключ в мой образ мыслей. Тем не менее, я хочу узнать, как это сделать должным образом. Просто потому, что это работает для меня прямо сейчас, не означает, что я должен продолжать делать это таким образом.

Вы упоминаете использование фабрик. Но разве фабрики не разрушают DI, потому что они создают объекты?

Вы также можете заметить, что мои модели несут ответственность за взаимодействие с базой данных. Я подозреваю, что это также проблема с моим дизайном. Должен ли я использовать сопоставление данных какого-либо типа, чтобы удалить эту ответственность из моих моделей?

Учитывая этот более конкретный пример того, как я в настоящее время реализую свой код, есть ли у вас какие-либо другие рекомендации или вы можете проиллюстрировать, как я должен делать это по-другому?

Ответ 1

Не глядя на конкретные реализации, трудно дать конкретные советы, поэтому вместо этого я предоставлю общий совет, который я могу (на самом деле я не очень хорош). Похоже, что вы можете использовать "контейнер инъекций зависимостей" (google it.. найдите тот, который вам нравится или катите свой собственный для своих целей), который управляет большими взаимосвязанными зависимостями классов.

Одна вещь, о которой вы должны помнить, заключается в том, что ваш код должен осторожно падать вокруг дизайна, что очень сильно задержит его. Вам не нужно много работать, чтобы заставить ваш код следовать за созданным вами дизайном. Если то, что вы пытаетесь сделать, не работает в соответствии с вашим пониманием дизайна, это, вероятно, время для перезаписи.

Вы также используете только примеры впрыска конструктора. Я уверен, что вы знаете, что при необходимости можно использовать другие виды инъекций, такие как инъекция сеттера.

Как я уже сказал, я не видел ваш код, но проблема с вашим дизайном заключается в том, что ему не нужна страница, которая должна знать о том, какие виджеты ему нужны, это сервис. Если вы привязываете конкретные реализации виджета к странице, это нарушает инверсию зависимостей (и делает более сложной инъекцию зависимостей). Вместо этого ваш код должен быть больше по строкам:

interface Widget {
   function widgetize();
}

class ConcretePage {
   private $widgets = array();

   public function addWidget(Widget $widget) {
      $this->widgets[] = $widget;
   }

   public function run() {
      foreach ($this->widgets as $widget) {
         $widget->widgetize();
      }
   }
}

Служба создаст страницу и добавит необходимые ей виджеты. Страница не должна заботиться об этих виджетах. Возможно, вам также придется реализовать мост между страницей и виджетами. Служба также может справиться с этим.

В качестве автора статьи вы упоминаете здесь, у вас должен быть код, предназначенный для построения всего вашего графа объектов (здесь все ваших операторов new). Представьте себе, насколько сложно было бы проверить вашу страницу, которая ожидает пять конкретных виджетов без определения классов этих виджетов! Вы должны зависеть от абстракций.

Это может быть не в духе DI, но более простым решением было бы иметь WidgetFactory:

class Page {
    private $wf;
    private $widgets = array('whiz', 'bang');
    public function __construct(WidgetFactory $wf) {
       $this->wf = $wf;
    }
    public function widgetize() {
       foreach ($this->widgets as $widget) {
          $widget = $this->wf::build($widget);
       }
    }
}

Интерфейс WidgetFactory может быть отделен от приложения к приложению и даже при тестировании. Это все еще дает вам некоторый уровень абстракции для ожидаемой страницы виджетов.

Ответ 2

У меня инъекции зависимостей полезны только потому, что я могу выбрать между синтаксисом $this- > method(); или $obj- > method(); для вызова того же метода. Это можно сделать, используя набор магических функций, get и call.

Ответ 3

Извините за то, что так долго отвечал, но мне пришлось много подумать об этом. Еще раз, это не правильно, это чисто мое мнение и спекуляции. Это ответ на ответ в вашем вопросе:

В некотором роде ваша страница должна знать, какие виджеты ему нужны (она, по-видимому, получает их от конкретного запроса БД). Однако на самом деле он не должен заботиться о том, какие виджеты он получает. Также не обязательно, чтобы страница была привязана к виджетам (например, как член, даже если это противоречит моему предыдущему ответу).

На самом деле, я думаю, что ваш новый код в основном в порядке. У вас есть factory для виджетов, хотя он привязан к определенной странице. Единственной проблемой, которую я хотел бы отметить, является создание new PhotoGallery в классе PhotoGallery_Widget. Вы не зависите от абстракции, но конкреции, и еще хуже создать этот объект в методе. Вы можете создать эти требуемые объекты с помощью другого factory в контроллере.