ХабрПостыНовостиМегапосты?

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

Я регулярно вижу статьи в стиле "как использовать шаблон Репозиторий с 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();
    }
}

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


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



Чистый 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);
    }
}

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


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


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


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



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


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


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


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


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


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


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


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

@Adelf
21.03.2019 16:34 UTC
Первоисточник