Пожалуйста, прекращайте говорить про шаблон Репозиторий с Eloquent

Автор оригинала: Adel F
  • Перевод

Я регулярно вижу статьи в стиле "как использовать шаблон Репозиторий с Eloquent" (одна такая попала в недавний PHP-дайджест). Обычное содержание их: давайте создадим интерфейс PostRepositoryInterface, EloquentPostRepository класс, красиво их забиндим в контейнере зависимостей и будем использовать вместо стандартных элоквентовских методов save и find.


Зачем этот шаблон нужен иногда вовсе не пишут ("Это же шаблон! Разве не достаточно?"), иногда что-то пишут про возможную смену базы данных (очень частое явление в каждом проекте), а также про тестирование и моки-стабы. Пользу от введения такого шаблона в обычный Laravel проект в таких статьях сложно уловить.


Попробуем разобраться, что к чему? Шаблон Репозиторий позволяет абстрагироваться от конкретной системы хранения (которая у нас обычно представляет собой базу данных), предоставляя абстрактное понятие коллекции сущностей.


Примеры с Eloquent Repository делятся на два вида:


  1. Двойственная Eloquent-array вариация
  2. Чистый Eloquent Repository

Двойственная Eloquent-array вариация


Пример первого (взят из рандомной статьи):


<?php
interface FaqRepository
{
  public function all($columns = array('*'));

  public function newInstance(array $attributes = array());

  public function paginate($perPage = 15, $columns = array('*'));

  public function create(array $attributes);

  public function find($id, $columns = array('*'));

  public function updateWithIdAndInput($id, array $input);

  public function destroy($id);
}

class FaqRepositoryEloquent implements FaqRepository
{
  protected $faqModel;

  public function __construct(Faq $faqModel)
  {
      $this->faqModel = $faqModel;
  }

  public function newInstance(array $attributes = array())
  {
      if (!isset($attributes['rank'])) {
          $attributes['rank'] = 0;
      }
      return $this->faqModel->newInstance($attributes);
  }

  public function paginate($perPage = 0, $columns = array('*'))
  {
      $perPage = $perPage ?: Config::get('pagination.length');

      return $this->faqModel
          ->rankedWhere('answered', 1)
          ->paginate($perPage, $columns);
  }

  public function all($columns = array('*'))
  {
      return $this->faqModel->rankedAll($columns);
  }

  public function create(array $attributes)
  {
      return $this->faqModel->create($attributes);
  }

  public function find($id, $columns = array('*'))
  {
      return $this->faqModel->findOrFail($id, $columns);
  }

  public function updateWithIdAndInput($id, array $input)
  {
      $faq = $this->faqModel->find($id);
      return $faq->update($input);
  }

  public function destroy($id)
  {
      return $this->faqModel->destroy($id);
  }
}

Методы all, find, paginate возвращают Eloquent-объекты, однако create, updateWithIdAndInput ждут массив.


Само название updateWithIdAndInput говорит о том, что использоваться этот "репозиторий" будет только для CRUD операций.


Никакой нормальной бизнес-логики не предполагается, но мы попробуем реализовать простейшую:


<?php
class FaqController extends Controller
{
    public function publish($id, FaqRepository $repository)
    {
        $faq = $repository->find($id);

        //...Какая-нибудь проверка с $faq->... 
        $faq->published = true;

        $repository->updateWithIdAndInput($id, $faq->toArray());
    }
}

А если без репозитория:


<?php
class FaqController extends Controller
{
    public function publish($id)
    {
        $faq = Faq::findOrFail($id);

        //...Какая-нибудь проверка с $faq->... 
        $faq->published = true;

        $faq->save();
    }
}

Раза в два проще.


Зачем вводить в проект абстракцию, которая только усложнит его?


  • Юнит тестирование?
    Каждому известно, что обычный CRUD-проект на Laravel покрыт юнит-тестами чуть более чем на 100%.
    Но юнит-тестирование мы обсудим чуть позже.
  • Ради возможности сменить базу данных?
    Но Eloquent и так предоставляет несколько вариантов баз данных.
    Использовать же Eloquent-сущности для неподдерживаемой им базы для приложения, которое содержит только CRUD-логику будет мучением и бесполезной тратой времени.
    В этом случае репозиторий, который возвращает чистый PHP-массив и принимает тоже только массивы, выглядит намного естественнее.
    Убрав Eloquent, мы получили настоящую абстракцию от хранилища данных.

Чистый Eloquent Repository


Пример репозитория с работой только с Eloquent(тоже нашёл в одной статье):


<?php

interface PostRepositoryInterface
{
    public function get($id);

    public function all();

    public function delete($id);

    public function save(Post $post);
}

class PostRepository implements PostRepositoryInterface
{
    public function get($id)
    {
        return Post::find($id);
    }

    public function all()
    {
        return Post::all();
    }

    public function delete($id)
    {
        Post::destroy($id);
    }

    public function save(Post $post)
    {
        $post->save();
    }
}

Не буду в этой статье ругать ненужный суффикс Interface.


Эта реализация чуть больше походит на то, о чем говорится в описании шаблона.


Реализация простейшей логики выглядит чуть более натурально:


<?php
class FaqController extends Controller
{
    public function publish($id, PostRepositoryInterface $repository)
    {
        $post = $repository->find($id);

        //...Какая-нибудь проверка с $post->... 
        $post->published = true;

        $repository->save($post);
    }
}

Однако, реализация репозитория для простейших постов в блог — это игрушка детишкам побаловаться.


Давайте попробуем что-нибудь посложнее.


Простая сущность с подсущностями. Например, опрос с возможными ответами (обычное голосование на сайте или в чате).


Кейс создания объекта такого опроса. Два варианта:


  • Создать PollRepository и PollOptionRepository и использовать оба.
    Проблема данного варианта в том, что абстракции не получилось.
    Опрос с возможными ответами — это одна сущность и ее хранение в базе должно было быть реализовано одним классом PollRepository.
    PollOptionRepository::delete будет непростым, поскольку ему нужен будет обьект Опроса, чтобы понять можно ли удалить данный вариант ответа (ведь если у опроса будет всего один вариант это будет не опрос).
    Да и не предполагает шаблон Репозиторий реализацию бизнес-логики внутри репозитория.
  • Внутри PollRepository добавить методы saveOption и deleteOption.
    Проблемы почти те же. Абстракция от хранения получается какая-то куцая… о вариантах ответа надо заботиться отдельно.
    А что если сущность будет еще более сложная? С кучей других подсущностей?

Возникает тот же вопрос: а зачем это все?
Получить большую абстракцию от системы хранения, чем дает Eloquent — не получится.


Юнит тестирование?


Вот пример возможного юнит-теста из моей книгиhttps://gist.github.com/adelf/a53ce49b22b32914879801113cf79043
Делать такие громадные юнит-тесты для простейших операций мало кому доставит удовольствие.


Я почти уверен, что такие тесты в проекте будут заброшены.


Никто не захочет их поддерживать. Я был на проекте с такими тестами, знаю.


Гораздо проще и правильнее сосредоточиться на функциональном тестировании.
Особенно, если это API-проект.


Если же бизнес-логика так сложна, что очень хочется покрыть ее тестами, то лучше взять data mapper библиотеку вроде Doctrine и полностью отделить бизнес-логику от остального приложения. Юнит-тестирование станет раз в 10 проще.


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

Поделиться публикацией

Похожие публикации

Комментарии 33

    +1
    Дико плюсую автору! Хватит впиливать в проекты сложноподдерживаемые решения и потом мучиться с ними. Если так сильно хочется использовать репозитории, то переходите на Symfony. Это позволит сократить время, которое вам потребуется, чтобы поддерживать свой велосипед на репозиториях.
      +1
      Начал за здравие, кончил за упокой. Я тоже полностью согласен с автором, но при чем тут Symfony? Это проблема модели взаимодействия с бд AR а не фреймворка.
        +2
        Люди часто доктрину связывают с симфони. Я спокойно юзаю доктрину в ларавель проекте…
          +1
          переходите на Symfony
          при чем тут Symfony?
          Люди часто доктрину связывают с симфони

          следующий вопрос: причем тут доктрина? )

            +3

            Оратор выше, видимо, имеет ввиду, что репозитории с Data Mapper работают прекрасно, в отличии от репозиториев с Active Record.
            Doctrine — единственная более менее съедобная имплементация Data Mapper на PHP.


            Я же указываю на то, что Doctrine является самостоятельным проектом, разработка которого напрямую никак не связана с Symfony (косвенно может и связана — они там все общаются же).


            В общем, переходить на Symfony, просто потому что там Doctrine — глупо (у вас должны быть более веские основания чтобы сменить фреймворк, в котором уже наработан опыт), потому как Doctrine точно также подключается и в Laravel. Я даже знаю человека, который наоборот цеплял Eloquent (illuminate/database) в проект на symfony. Ума не приложу, зачем это ему, но речь не об этом. Я всего лишь хотел сказать, что эти конкретные ORM не завязаны на фреймворк, как таковые. И, при желании, они легко запиливаются/выпиливаются (хотя, с выпиливанеим Eloquent из Laravel, все несколько сложнее, но не безнадежно).

              0

              А вот оно что — вы призыв перехода на симфони связали с тесной интеграцией с ней доктрины (хотя это прямо нигде не указано).
              И тут же пишете, что "Doctrine является самостоятельным проектом" и "ORM не завязаны на фреймворк", хотя тред именно с вашей подачи идет в интерпретации "Симфони = Доктрина".

                0
                Если так сильно хочется использовать репозитории, то переходите на Symfony.

                Это не я сказал. Репозитории можно использовать в том фреймворке, в котром вам вкусно их использовать. Но не с любой ORM.


                P.S. я не понимаю в чем вы хотите меня уличить.
                P.P.S Можно использовать репозтории даже с Eloquent, если считать, что модели Eloquent это не модели предметной области, а их маппинг на базу данных.
                В этом случае, у вас полчится забавный каламбур с DM работающим поверх AR.

                  +1
                  Хватит впиливать в проекты сложноподдерживаемые решения и потом мучиться с ними. Если так сильно хочется использовать репозитории, то переходите на Symfony.

                  лично я понял это как отсылку к излишней академичности решений на симфони. Ведь репозитории действительно можно использовать везде, а репозитории Доктрины — это явно не те абстрактные репозитории, о которых идет речь в статье.

                    0
                    Хм… а я последнее предложение понял буквально. Я почему-то подумал, что «сложноподдерживаемые решения» — это именно репозитории с AR, а не репозитории вообще.
                    Я уж к холивару приготовился. А тут просто недопонимание )
      0
      — регулярно вижу статьи в стиле «как использовать шаблон Репозиторий с Eloquent»

      Впервые о таком слышу. Статей ни одной не видел.

        0
        Вон в PHP-дайджесте недавнем была — habr.com/en/post/441584. Это кстати и стало поводом для написания этой статьи.
        +2
        одна такая попала в недавний PHP-дайджест

        Тоже негодовал по этому поводу (особенно с тем решением, что предлагалось в статье)


        Не буду в этой статье ругать ненужный суффикс Interface

        И не нужно ругать. Как-никак, PSR Naming Conventions


        А в целом, согласен с автором.

          0
          Внесу небольшую поправку по поводу PSR Naming Conventions: это внутренний документ, регулирующий правила именования для самих PSR. То есть один из стандартов для публикуемых рекомендаций.
          PSR Naming Conventions предназначен для разработчиков PSR и не оформлен сам как PSR, а потому он не является рекомендацией для нейминга теми проектами, которые используют PSR.

          UPD. Впрочем как и Symfony Conventions — это, соответственно, тоже правила и конвенции, предназначенные для разработчиков и контрибьютеров Symfony
            +1
            Всегда добавляю суффикс Interface (за исключением тех случаев, когда название интерфейса является прилагательным). Но PSR Naming Conventions тут вообще не причем. Просто в глобальном поиске IDE (PHP Storm в моем случае) легче отличать интерфейс от имплементации. Вот такой вот я плохиш )
              0
              А зачем их отличать?
              Видел и NameInterface, и NameContract, и IName и даже I_Name, но никокого практического смысла так из этих приемов и не извлек.
                +1

                Суффикс Interface — это венгерская нотация для более высокого уровня абстракции. Раньше тип переменных включали в имя переменной (intCount, strName, и т.д.). Теперь возможности языка и IDE таковы, что в венгерской нотации нет необходимости, в том числе и для интерфейсов. Почему мы все классы в проекте суффиксом Class не награждаем? Пора сделать код более читабельным и отказаться от мусорного суффикса для интерфейсов в том числе. Список литературы:
                https://www.alainschlesser.com/interface-naming-conventions/


                https://dev.to/scottshipp/when-hungarian-notation-lies-hidden-in-plain-sight-372


                https://phpixie.com/blog/naming-interfaces-in-php.html


                https://twitter.com/nikolaposa/status/1077327810627358721

                  –1

                  интерфейс легко отличается по иконке

                    +3

                    Всё верно, но бывает, что он даже не попадает в первый экран выдачи. А когда ты добавляешь заветное I, то оказывается прямо под рукой. Я например, для перехода к CacheInterface забиваю CaI, и шторм махом понимает что именно я хочу. В то время, как если бы это был просто Cache, то я бы собрал всю папку vendor и все, что там называется Cache, и не факт что интерфейс оказался бы в первом экране выдачи.

                      0
                      А если нужно посмотреть код из консоли?
                        0

                        то у вас появляется еще куча проблем, которые можно, но не нужно, решать именованием файлов/классов.

                  0
                  Не согласен с автором. Лучше прикрутить репозитории, чем писать eloquent запросы в контроллере. Мы же хотим использовать те же запросы и в других местах тоже? А ещё когда понадобится прикрутить кеширование? Если есть репозиторий то добавишь декораторы и будет удобное кеширование
                    0
                    Если же у вас в проекте Eloquent и очень хочется побаловаться шаблонами проектирования, то в следующей статье я покажу как можно частично применить шаблон Репозиторий и получить от этого пользу.

                    Как раз про это будет.
                    +1
                    Если честно не понял статью (может просто пятница?): Вроде призыв к не работе в Eloquent в концепции репозитория. Но предложения составлены так, что это «не» не видно. Много кода, но все для «не» делайте так. А если нужно, альтернативы не прописано.

                    Зачем вводить в проект абстракцию, которая только усложнит его?

                    Потому, что если действие повторяется, при этом есть другой код, а не просто save, то логично его вынести отдельно, а не в каждом контроллере повторять — Но не в Репозитории, а в Сервис.

                    Почему не Репозитории. В 99% случае в модели нужно описывать методы с отношениями к другим таблицам. Т. е. Ломается логика в получении данных не через репозитории зависимой модели, а через встроенный обработчик отношений.
                      0
                      ну вы очень кратко как раз статью и пересказали :)
                        0
                        Вот, собственно, это и «плохо».
                        0
                        ну в какой-нибудь доктрине тоже отношения часто через прокси реализованы. смысл в персисте. во всех реализациях Active Record — приходится вручную их в базу кидать, либо минуя репозиторий, либо использовать другой «репозиторий».
                        +1
                        Поддерживаю, никогда не понимал сакрального смысла репозитория возвращающего eloquent модели
                          +2
                          Я отчасти понимаю негодование автора, НО автор слишком заостряет внимание на шаблоне.
                          1. Шаблон это рекомендация.
                          2. Помимо разделения и тестирования есть ещё жизненный цикл. И подобный подход позволяет в определённой степени поддерживать и обновлять продукт гораздо проще со всеми итерациями рефакторинга и дальнейшей разработкой. С тестированием да, возникают проблемы.
                          3. Автору оригинала задавали уйму вопросов в комментариях, в том числе из разряда, что ваш репозиторий — не репозиторий вовсе.
                          Дочитывая пост до конца, остаётся чувство, будто на тебя вылили ушак помоев, но извиниться забыли. Зачем автор вместо того, чтобы писать пост «программистского гнева», который в целом понятен и я отчасти его поддерживаю, не предложил сразу альтернативный вариант, который отвечает всем «критериям» автора?
                          В любом случае, спасибо за материал, жду альтернативный подход.
                            0
                            Пожалуй, с этой точки зрения действительно стоило не превращать это в две статьи, а написать все в одной…
                            0
                            Эта тема стабильно поднимается и поднимается. Несколько лет назад я споткнулся где-то рядом habr.com/ru/post/316836.

                            Возникает тот же вопрос: а зачем это все?


                            Попробую засветить мысли к которым я пришел. Дело в том, что есть множество RAD-разработчиков, которые практически ничего не использовали за рамками Laravel5 и/или Yii2.
                            Однако некоторые из них слышали про существование других инструментов, паттернов и тп.
                            А еще некоторые из некоторых хотят развиваться и далеко не у всех есть верховный маг PHP стоящий выше.

                            Следовательно такие разработчики читают про что-то новое, в особенности паттерны и бегут это сразу же применять (тут я могу и себе подмигнуть).

                            Так вот, почти всем нам хорошо известно, что ActiveRecord нарушает принципы SOLID.
                            Поэтому, если наш AR Repository будет возвращать такую штуку как Model, то можно сделать следующее:

                            <?php
                                $post = $repository->find($id);
                                //...Какая-нибудь проверка с $post->... 
                                $post->published = true;
                                $post->delete(); // или $post->save() // или $post->{любая магия AR}
                            
                                $repository->save($post);
                            


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

                            Что касается смены базы данных, поддержки разных баз в AR и тп. Тут нужно кейс рассматривать совсем в другом ключе. Репозиторий это абстракция над хранилищем, а хранилище может быть в другом источнике. Например у вас есть блог на сайте и мы берем данные из нашей базы, а затем у нас появляется кейс, брать данные не из нашей базы, а уже из внешнего ресурса, например стороннего API (какой-то там биржи статей или другого блога). Тогда в теории нам нужно подменить только реализацию, что бы все завелось.

                            Однако, если у нас нет сущностей и мы гоняем AR Model в репозиториях, то тогда возникнут большие проблемы.

                            Собственно ключевая особенность RAD-инструмента от Enterprise, как раз в простоте.
                            Как когда-то мне сказал zelenin, выбирая инструмент нужно принимать правила игры.

                            Но есть еще 1 момент. Можно использовать Repository не как паттерн абстракции над хранилищем в рамках RAD-инструмента, а как некий класс в котором будут лежать наши запросы для удобства. В таком случае нам и интерфейс не нужен:

                            <?php
                            class UserRepository
                            {
                                public static function findActiveUsers(): User
                               {
                               }
                            
                                public static function findBestUser(): User
                               {
                               }
                            
                                public static function findBannedUsers(): Users
                               {
                               }
                            
                            ...
                            }
                            


                              0
                              Но есть еще 1 момент. Можно использовать Repository не как паттерн абстракции над хранилищем в рамках RAD-инструмента, а как некий класс в котором будут лежать наши запросы для удобства. В таком случае нам и интерфейс не нужен

                              в таком случае это не репозиторий, а сервис

                                +2
                                Максимально точно выражено мое мнение, полностью поддерживаю. Но необходимо дополнить.

                                Использование репозитория просто как абстракции над хранилищем зачастую не имеет смысла в продуктах до определенного размера. Тут проблема в том, что любое изменение бизнес-условий в продукте чуть выше размером, чем домашняя CRUD страничка, вызовет мучительные боли чуть пониже спины, потому что эти изменения коснутся каждого файла проекта, где подобные операции используются.
                                К примеру, буквально вчера в большой лигаси-системе я зарелизил небольшой блок, который касался добавления нового типа сущности. Проблема в том, что этот тип сущности требовалось получить из внешнего источника, отличного от базы данных. Кроме того, его нужно было зарегистрировать во всех элементах сайта, где он может потенциально использоваться. Итого, мне пришлось изменить что-то в районе 80 файлов на стороне сервера, чтобы добавить тривиальную функцию. А это непаханое поле ошибок и потенциальных багов, которые необходимо еще отлавливать.

                                Используй разработчик паттерн репозиторий, да и в целом, поддерживай он исходники в более качественном состоянии, подмена кода была бы не такой страшной и менять пришлось бы не более 10-15 файлов.

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

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

                                Резюмируя. Если вы хотите сделать быстро, проверить гипотезы, не планируете вносить большие изменения от слова совсем — используйте Eloquent as is — это крутейший инструмент, значительно ускоряющий процесс разработки продукта. Но если вы делаете проект для заказчика, в условиях изменяющихся бизнес-требований, с учетом необходимости длительной поддержки сервиса, не поленитесь, пишите репозитории: они окупятся. И дело даже не в смене базы данных, а в целом, в простоте поддержки продукта на разных этапах его существования.
                                  0
                                  Полностью согласен. И даже коротко примерно описал:
                                  Если же бизнес-логика так сложна, что очень хочется покрыть ее тестами, то лучше взять data mapper библиотеку вроде Doctrine и полностью отделить бизнес-логику от остального приложения. Юнит-тестирование станет раз в 10 проще.
                                  Полностью отделенная логика предполагает абстракции вроде Репозиториев. По крайней мере, для write моделей.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое