Мне любопытно, как другие разработчики работают с фреймворком Laravel. Я видел выступление Adam Wathan о написании кода контроллера ресурсов и о том, насколько просто / чисто он выглядит.
Я хотел бы поделиться с сообществом тем, как они работают с Laravel. Мне бы хотелось узнать что-то новое и посмотреть, что я могу улучшить с помощью моих шаблонов проектирования.

В моем коде прямо сейчас я использую следующий подход:

Controller -> Service -> Repository -> Model

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

Routes


Мне нравится использовать Laravel Resource Controllers. В качестве примера давайте создадим страницу со списком пицц (index). Я также добавил два примера, чтобы показать вложенную страницу заказа относящуюся только к пицце. (страница для создания, а затем, наконец, страница для сохранения заказа).

Route::resource('/pizzas', 'PizzaController', ['only' => [
   'index',
]]);

Route::group(['prefix' => 'pizzas'], function() {
   Route::resource('/orders', 'Pizza\OrderController', ['only' => [
       'create', 'store',
   ]]);
});

Итоговые маршруты:

GET /pizzas
App\Http\Controllers\PizzaController@index

GET /pizzas/orders/create
App\Http\Controllers\Pizza\OrderController@create

POST /pizzas
App\Http\Controllers\Pizza\OrderController@store


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

  • Pizza\OrderController — несет ответственность только за обработку заказов пиццы
  • PizzaController — несет ответственность за обработку деталей для пиццы

Controllers


Как вы можете увидеть выше, я стараюсь использовать только 7 методов, как это предлагается в документации Laravel для Resource Controllers:

  • index()
  • create()
  • store()
  • show()
  • edit()
  • update()
  • destroy()

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

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

public function index(PizzaService $pizzaService)
{
   return view('pizza.index', [
       'pizzas' => $pizzaService->all(),
   ]);
}

Примечание: view также соответствуют тому же шаблону папок, что и пространства имен.

Services


Мне нравится использовать Сервисы для обработки логики в моих приложениях. Сервис для меня может быть концепцией Domain Driven или 1-к-1 с помощью модели (таблицы базы данных). У меня есть абстрактный класс, который обрабатывает общие методы, которые я много использую в моих Сервисах. (Примечание: комментарии / dockblock удалены в примерах кода)

<?php

namespace App\Services;

abstract class BaseService
{
   public $repo;

   public function all()
   {
       return $this->repo->all();
   }

   public function paginated()
   {
       return $this->repo->paginated(config('paginate'));
   }
   public function create(array $input)
   {
       return $this->repo->create($input);
   }
   public function find($id)
   {
       return $this->repo->find($id);
   }

   public function update($id, array $input)
   {
       return $this->repo->update($id, $input);
   }

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

Поэтому мой Domain / Model based Service выглядит так:

<?php

namespace App\Services;

use App\Repositories\PizzaRepository;

class PizzaService extends BaseService
{
   private $pizzaRepository;

   public function __construct(PizzaRepository $pizzaRepository)
   {
       $this->pizzaRepository = $pizzaRepository;
   }
}

В этом PizzaService я могу добавить свои собственные методы, специфичные для логики, которую я пытаюсь реализовать. В продолжении страницы списка пицц $pizzaService->all() вызывает метод all() в BaseRepository, поскольку мы не перезаписываем его.

Repositories


Репозитории в моем коде — это в основном методы, которые используют Eloquent для получения или записи данных в БД. Только Сервис может вызывать уровень репозитория. (Я сомневался в этом подходе, но сейчас я всегда стараюсь следовать ему).

<?php
namespace App\Repositories;

use Illuminate\Database\Eloquent\Model;

abstract class BaseRepository
{
   public $sortBy = 'created_at';
   public $sortOrder = 'asc';
   public function all()
   {
       return $this->model
            ->orderBy($this->sortBy, $this->sortOrder)
            ->get();
   }

   public function paginated($paginate)
   {
       return $this
           ->model
           ->orderBy($this->sortBy, $this->sortOrder)
           ->paginate($paginate);
   }

   public function create($input)
   {
       $model = $this->model;
       $model->fill($input);
       $model->save();

       return $model;
   }

   public function find($id)
   {
       return $this->model->where('id', $id)->first();
   }

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

   public function update($id, array $input)
   {
       $model = $this->find($id);
       $model->fill($input);
       $model->save();

       return $model;
   }
}

Итак, PizzaRepository, который загружается PizzaService, выглядит так:

<?php

namespace App\Repositories;

use App\Models\Pizza;

class PizzaRepository extends BaseRepository
{
   protected $model;

   public function __construct(Pizza $pizza)
   {
       $this->model = $pizza;
   }
}

Следует также отметить, что в моих Сервисах и Репозиториях, если мне нужно, я могу перезаписать методы по умолчанию, чтобы использовать мою собственную реализацию. Вы помните ранее в нашем примере списка пицц, BaseService вызывал метод all() в репозитории. Теперь, поскольку PizzaRepositoryis не перезаписывает BaseRepository, он использует метод all() в BaseRepository, чтобы вернуть список всех пицц из базы данных.

В качестве примера при перезаписи метода BaseRepository один из моих методов использует хранимые процедуры для вставки данных, поэтому я мог бы перезаписать метод create из BaseRepository, например так:

<?php

namespace App\Repositories;

use App\Models\Pizza;

class PizzaRepository extends BaseRepository
{
   protected $model;

   public function __construct(Pizza $pizza)
   {
       $this->model = $pizza;
   }
   public function create(array $input)
   {
       return $this->model->hydrate(
           DB::select(
               'CALL create_pizza(?, ?)',
               [
                   $name,
                   $hasCheese,
               ]
           )
       );
   }
}

Это простой пример, но теперь я возвращаю гидратированный результат из моей хранимой процедуры.

Traits


Я просто ввел идею Трейтов в мой код. Это произошло, когда я обнаружил, что некоторые из моих слоев репозитория нуждаются в возможности настроить сортировку (вы заметите, что у моего BaseRepository есть два свойства sortBy и sortOrder. Поэтому я создал признак Sortable. Теперь я могу сортировать страницу с списком пицц этими свойствами.

<?php

namespace App\Repositories\Traits;

trait Sortable
{
   public $sortBy = 'created_at';

   public $sortOrder = 'asc';

   public function setSortBy($sortBy = 'created_at')
   {
       $this->sortBy = $sortBy;
   }

   public function setSortOrder($sortOrder = 'desc')
   {
       $this->sortOrder = $sortOrder;
   }
}

Итак, теперь в моем Сервисе, где я применил Трейт, я могу установить сортировку. (Пример ниже устанавливает порядок в конструкторе, но вы также можете запустить этот метод в своих настраиваемых методах Сервиса.)

<?php

namespace App\Services;

use App\Repositories\PizzaRepository;

class PizzaService extends BaseService
{
   private $pizzaRepository;

   public function __construct(PizzaRepository $pizzaRepository)
   {
       $this->pizzaRepository = $pizzaRepository;
      
       $this->pizzaRepository->setSortBy('sort_order');
   }
}

У меня также были некоторые трудности с попыткой выяснить, как справиться с жадной загрузкой. Мне не понравилась идея вернуть данные моему контроллеру, а затем использовать ленивую жадную загрузку. Это было не слишком удобно для оптимизации запросов к базе данных. Как только я сделал Sortable Трейт, я решил сделать подобный трейт Relationable.

<?php

namespace App\Repositories\Traits;

trait Relationable
{
   public $relations = [];

   public function setRelations($relations = null)
   {
       $this->relations = $relations;
   }
}

Затем я добавил метод with () в мои методы BaseRepository:

public function all()
{
   return $this->model
       ->with($this->relations)
       ->orderBy($this->sortBy, $this->sortOrder)
       ->get();
}

Через мой сервис я могу добавить следующий код к любому методу (orders — это метод отношений модели).

$this->repo->setRelations([‘orders’]);

Я беспокоюсь, что с течением времени может быть легко усложнить мое приложение со слишком многими Трейтами, но сейчас оно работает очень хорошо.

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


  1. parotikov
    09.03.2018 14:00

    Ваш репозиторий должен возвращать простую коллекцию объектов (один объект), вместо коллекции моделей (одной модели), т.к. модели потом гуляют у вас по коду и разработчик может в обход репозитория удалить/сохранить модель. Т.е. теряется суть репозитория как слоя взаимодействия с моделью, с тем же успехом можно напрямую из сервиса модель дергать


    1. yushkevichv Автор
      09.03.2018 14:21

      Спасибо за обратную связь, попозже изучу внимательнее ваш пример.


      Эта статья — перевод.


      1. php7
        10.03.2018 10:55

        Мы понели, что мопед — не твой.


  1. parotikov
    09.03.2018 14:15

    Вот хорошая статья, подробнее объясняющая мой комментарий: habrahabr.ru/post/316836


  1. r-moiseev
    09.03.2018 16:10
    +1

    Есть такая хорошая архетиктура, обделенная статьей на хабре — Proto, реализованная на Laravel в проекте Apiato


    Суть примерно та же. Голый MVC в том виде в котором он есть в Laravel недостаточен.


    1. yushkevichv Автор
      09.03.2018 16:44

      Спасибо большое за ссылки. Интересно будет посмотреть.


  1. boodda
    09.03.2018 20:22

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


    По пунктам:


    • Почему ваш код находится в пространстве App\Http\Controllers ?


    • Если речь идёт про DDD. То надо было реализовать всю логику приложения в отдельном пространстве. Вообще не в папке с Laravel. Где то в /src/. Вот туда надо было складывать ваши абстрактные репозитории. А на Laravel лишь реализовывать адаптеры, по интерфейсам. А вообще, делать универсальный репозиторий — идея так себе. На практике это не работает. Хоть они и похожи, всегда есть отличия. Тут надо событие бросить, тут что то не сохранять. В итоге каждый репозиторий будет иметь методы, которые будут перегружать методы родительского класса. Лучше делать отдельный, несвязанный реп, к каждой из сущностей. Так будет проще впоследствии.


    • У вас репозиторий сетит в себя модель при инстанцировании, что потом приведет к проблемам. Что делать если вы захотите создать сразу 3 объекта Pizza?

    В create должна быть фабрика.


    $class = get_class($this->model); 
    $newObj = new $class();
    $newObj->fill($data);
    return $newObj;

    • Сортировка как свойство класса? Really. Ну на вкус и цвет конечно, но видать вы еще не усвоили, что использование явных параметров в функциях лучше, чем свойства объектов. Все потому что у вас в классе 6 методов, а сортировку используют лишь 2. Подумайте об этом.


    • $this->model->hydrate, опять же, используйте статичные фабричные методы. {Model}::hydrate() или по аналогии с п.3


    • Ну и ответ на вопрос: — как справиться с жадной загрузкой?
      Ответ: Не использовать. Да как бы привлекательно это ни было.
      На край — не должна покидать пределы репы.


    • Трейты с сортировкой в мусорку.


    1. yushkevichv Автор
      09.03.2018 21:10

      Добрый вечер.


      У меня масса проблем с переводом статьи? Или вы имели ввиду, что вам не нравится подход автора оригинальной статьи?


      Попробую ответить на ваши вопросы:
      1) не весь, а только код одного контроллера. Ещё два во вложенном namespace.
      Сервисы, репозитории и трейты находятся в отдельных пространствах. Уточните, в чем на ваш взгляд ошибка и как бы вы сделали?


      2) почему вы считаете, что логику приложения нужно делать не в папке с фреймворком? Не могли бы какой-нибудь пример подобной реализации показать. Пока не очень хорошо понимаю преимущества данного подхода.
      Насчёт абстрактных классов и трейтов- даже Фреймворк их активно использует и далеко не часто приходится перезагружать. Возможно у нас разный опыт, но я применял подобные паттерны с трейтами и абстрактными классами на не маленьких проектах и особых проблем не испытывал. Это не золотая пуля и многое зависит от изначального дизайна приложения. Согласен, что это может быть не оправданным.


      3) статья рассказывает о подходе, а не о конечном дизайне.
      От себя могу добавить, есть гипотеза, что на небольших проектах уровень репозитория не нужен — его вполне закрывает eloquent. А на больших проектах эту реализацию лучше заменить на doctrine.
      В любом случае, я считаю, что нужно на каждом проекте заниматься проектированием в начале проекта.


      4) я бы не хотел принимать это на свой счёт и быть категоричным. Есть количество способов решить эту задачу > 1. В каждой конкретной ситуации лучшими могут быть разные.


      5) поясните, почему вы так считаете?


      6) а как вы обычно решаете проблему n+1? А разве в указанных примерах сортировка вне репозитория?
      Не уверен, что понимаю вашу мысль. Не могли мы это замечания описать детальнее?


      7) почему вы так считаете?


      1. boodda
        09.03.2018 23:06

        Опять. Снова пропустил тег перевод.

        1 и часть второго: Я несколько раз вкорячивался с разными библиотеками, и фреймворками. С laravel дваджы, когда менялось куча всего, и надо было много править. Теперь я не пишу бизнес-логику с внутри проекта на фреймворке. Бизнес-логика — отдельно, фреймворк как инфраструктура — отдельно.

        2. В долгую, копипаста лучше, чем 1 абстрактный и потомки с перегрузкой методов. Про трейты п.7.

        3. Eloquent закрывает только микропроекты. Так как это active record, просто он по сути предназначен для CRUD, тут да это прям инь и янь, но все остальное — это не очень удачные попытки заменить SQL синтаксис, не удобным(когда начинают монструозные запросы на нём писать) и бажным(issues по eloquent открыты всегда десятками) PHP кодом. И Doctrine конечно вроде мощнее, но та же проблема. Многословно, новый синтаксис.

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

        5. $this->model это что, это уже какой-то объект, кто его туда положил, почему он там, в каком он состоянии? Вы не можете сказать. Но ожидаете что он «чистый», чтобы породить новый объект. Вызов же статичного метода, гарантировано вернёт вам новый объект с установленными значениями. Короче это метод класса, не метод объекта. Ну по-крайней мере обычно все это предполагают.

        6. Я думаю, что если у вас по коду возникает проблема N+1, значит походу, пора отложить Eloquent(Он про CRUD) и перейти на SQL. Возможно DataMapper вас спасет. А может и нет.

        7. Сортировка — это свойство запроса, а не класса. Ну вот просто, что такое сортировка? может она быть у репозитория? Вот мне кажется нет, но это только моё мнение. Та же история с отношениями.


  1. boodda
    09.03.2018 23:05

    Удалено. Не в ту ветку.


  1. fpinger
    10.03.2018 04:03

    Всё это про «магию». Вечная борьба прямого кода и «магии», которую считают рефакторингом. Но последний делается по факту, а вот «магия» — это часто преждевременная оптимизация.

    Автор, переведённой статьи, изобрёл один из небольших разделов «магии» согласно того с чем ему приходится сталкиваться. Она работает на простых проектах. Где модели не содержат сколько-то сложных агрегаторов.

    Если принять то, что это «магия» и она может работать в какой-то группе приложения, то я соглашусь с её применением. Когда есть простые модели (контекст laravel, а не DDD) и их связи. Если разработчик один. Пусть.

    Но бывает всё сложнее. Бывает нужно передать в сервис несколько репозиториев, а то ещё и другой сервис. После сервиса нужно выполнить нечто инфраструктурное, например, отправить почту и/или пуши, логировать и т.д. на основе успешности выполнения сервиса. Добавим в абстракцию сервиса подписку на события?
    Может в сервисе потребоваться дополнительная валидация, так как она нужна не везде, а там где нужна и валидируются не только входящие данные запроса. Её то же повесим абстракцией на сервис?

    Но главная проблема — это когда в проекте джуниор или условный мидл начинает писать код по аналогии увиденной «магии», не думая о её востребованности в его части задачи. Ведь понимание чьей-то «магии» так завораживает. Хочется то же стать «магом». Это приятно и побочные эффекты сразу не видны. Причём когда джуниор видит, что «магия» в конкретном случае не работает, то начинается его магия, например, с прямым доступом к моделям (контекст laravel).

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

    И опять же, серебряной пули нет.


  1. Finesse
    10.03.2018 12:35

    ИМХО, вы сделали слишком сложную систему, убив простоту Laravel.


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


  1. php7
    11.03.2018 01:01

    А в интернетах кто-то понимает, как использовать Журавель?

    Есть нормальный мануал? Или все расчитаны на дебилов?


  1. Fractalzombie
    11.03.2018 02:43

    Примерно так же работаю.


  1. aveselov
    11.03.2018 19:37

    После прочтения этой статьи я понял что я никак не работаю с Laravel