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

в 11:34, , рубрики: laravel, php, repository pattern, Совершенный код

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

Автор: Adelf

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js