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