Мне любопытно, как другие разработчики работают с фреймворком 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’]);

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