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

Комментарии (29)


  1. evgwed
    21.03.2019 16:35

    Дико плюсую автору! Хватит впиливать в проекты сложноподдерживаемые решения и потом мучиться с ними. Если так сильно хочется использовать репозитории, то переходите на Symfony. Это позволит сократить время, которое вам потребуется, чтобы поддерживать свой велосипед на репозиториях.


    1. greabock
      21.03.2019 17:23

      Начал за здравие, кончил за упокой. Я тоже полностью согласен с автором, но при чем тут Symfony? Это проблема модели взаимодействия с бд AR а не фреймворка.


      1. Adelf Автор
        21.03.2019 17:37

        Люди часто доктрину связывают с симфони. Я спокойно юзаю доктрину в ларавель проекте…


        1. zelenin
          21.03.2019 20:10
          +1

          переходите на Symfony
          при чем тут Symfony?
          Люди часто доктрину связывают с симфони

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


          1. greabock
            21.03.2019 21:32

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


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


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


            1. zelenin
              21.03.2019 21:43

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


              1. greabock
                21.03.2019 21:47

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

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


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


                1. zelenin
                  21.03.2019 21:52
                  +1

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

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


                  1. greabock
                    21.03.2019 22:04

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


  1. webviktor
    21.03.2019 16:43

    — регулярно вижу статьи в стиле «как использовать шаблон Репозиторий с Eloquent»

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


    1. Adelf Автор
      21.03.2019 17:05

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


  1. trawl
    21.03.2019 18:06

    одна такая попала в недавний PHP-дайджест

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


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

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


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


    1. xotey83
      21.03.2019 18:57
      +1

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

      UPD. Впрочем как и Symfony Conventions — это, соответственно, тоже правила и конвенции, предназначенные для разработчиков и контрибьютеров Symfony


    1. greabock
      21.03.2019 19:13

      Всегда добавляю суффикс Interface (за исключением тех случаев, когда название интерфейса является прилагательным). Но PSR Naming Conventions тут вообще не причем. Просто в глобальном поиске IDE (PHP Storm в моем случае) легче отличать интерфейс от имплементации. Вот такой вот я плохиш )


      1. DaleMartinWatson
        21.03.2019 19:37

        А зачем их отличать?
        Видел и NameInterface, и NameContract, и IName и даже I_Name, но никокого практического смысла так из этих приемов и не извлек.


      1. catanfa
        21.03.2019 19:52
        +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. zelenin
        21.03.2019 20:17

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


        1. greabock
          21.03.2019 22:15
          +2

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


        1. trawl
          22.03.2019 06:06

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


          1. zelenin
            22.03.2019 06:51

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


  1. L0NGMAN
    22.03.2019 01:36

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


    1. Adelf Автор
      22.03.2019 09:55

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

      Как раз про это будет.


  1. n0wheremany
    22.03.2019 09:40

    Много слов.

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

    Зачем такие танцы с бубнами? Нужен ActiveRecord — используй его полностью, а не создавай себе геморрой в голове со скрещиванием ужа с ежом.


    1. Adelf Автор
      22.03.2019 09:55

      ну вы очень кратко как раз статью и пересказали :)


      1. n0wheremany
        22.03.2019 10:21

        Вот, собственно, это и «плохо».


    1. Adelf Автор
      22.03.2019 10:43

      ну в какой-нибудь доктрине тоже отношения часто через прокси реализованы. смысл в персисте. во всех реализациях Active Record — приходится вручную их в базу кидать, либо минуя репозиторий, либо использовать другой «репозиторий».


  1. zhulan0v
    22.03.2019 10:11

    Поддерживаю, никогда не понимал сакрального смысла репозитория возвращающего eloquent модели


  1. olafars
    22.03.2019 14:34

    Я отчасти понимаю негодование автора, НО автор слишком заостряет внимание на шаблоне.
    1. Шаблон это рекомендация.
    2. Помимо разделения и тестирования есть ещё жизненный цикл. И подобный подход позволяет в определённой степени поддерживать и обновлять продукт гораздо проще со всеми итерациями рефакторинга и дальнейшей разработкой. С тестированием да, возникают проблемы.
    3. Автору оригинала задавали уйму вопросов в комментариях, в том числе из разряда, что ваш репозиторий — не репозиторий вовсе.
    Дочитывая пост до конца, остаётся чувство, будто на тебя вылили ушак помоев, но извиниться забыли. Зачем автор вместо того, чтобы писать пост «программистского гнева», который в целом понятен и я отчасти его поддерживаю, не предложил сразу альтернативный вариант, который отвечает всем «критериям» автора?
    В любом случае, спасибо за материал, жду альтернативный подход.


    1. Adelf Автор
      22.03.2019 14:34

      Пожалуй, с этой точки зрения действительно стоило не превращать это в две статьи, а написать все в одной…