Laravel традиционно является MVC-фреймворком, но MVC не слишком хорошо масштабируется для крупных проектов. Обычно, в конечном итоге получается, что логика содержится во всех секциях: моделях, представлениях и контроллерах, и по мере развития приложения его становится почти невозможно тестировать. В книге Тейлора Отвелла упоминается шаблон репозитория, но даже он не способен решить эту проблему масштабируемости. После того, как коллега показал мне статью в блоге о шестиугольном шаблоне проектирования в Rails, я решил попробовать применить его в Laravel — результаты были потрясающие.
Основная идея заключается в том, что маршруты и контроллеры предназначены только для доставки. В контроллере нет никакой логики, и точка.
Контроллер похож на вашего почтальона. Он получает адрес (маршрут) и доставляет коробку (модель + представление) по вашему адресу. Ему не нужно беспокоиться о содержимом коробки. Но получается, что почтальон всё равно принимает решения. Например, если сейчас идет дождь, коробка может оказаться либо в вашем автомобиле, иначе, вероятно, на вашем крыльце.
Итак, где вы поместите вашу логику? Я называю её сценарием (scenario), но она еще называется примером использования (use cases).
Так давайте применим нашу концепцию сценария к нашему придуманному примеру о почтальоне. Вместо того, чтобы почтальон пытался выяснить, что делать с коробкой, он просто будет следовать строгому набору руководящих принципов заранее определенного сценария. Он не делает никаких предположений и делает именно то, что ему сказано. Таким образом, в сценарии, в котором идет дождь, согласно правилам, он должен оставить записку, объяснив ситуацию, и не оставлять коробку под дождём. Это означает, что почтальон не несет ответственности за любые решения. Он просто механизм доставки заказов в соответствии с данными ему инструкциями.
Реализация
Ниже приведены два класса: PHPUsersController
и PHPUserScenario
. Это примеры, показывающие, каким образом можно создать в Laravel пользователя с помощью нашего шестиугольного шаблона со сценариями.
<?php
class UserController extends BaseController
{
public function __construct(UserScenario $scenario)
{
$this->scenario = $scenario;
$scenario->controller = $this;
}
public function create()
{
// здесь не нужна логика, мы всегда показываем emptyUser представлению user.create
$user = $this->scenario->emptyUser();
$this->layout->nest('content', 'user.create', compact('user'));
}
public function store()
{
// возвращаем createUserSuccess или createUserInvalid в зависимости от результата сценария
return $this->scenario->createUser(Input::get());
}
protected function createUserInvalid($validator, $input)
{
return Redirect::action('UserController@create')->withErrors($validator)->withInput($input);
}
protected function createUserSuccess($user)
{
Auth::loginUsingId($user->id);
return Redirect::action('HomeController@dashboard');
}
}
<?php
class UserScenario extends BaseScenario
{
/**
* Я мог бы запросто использовать здесь репозиторий, но для этого
* примера я собираюсь использовать обычную Eloquent-модель. Я думаю,
* что репозитории - пример предварительной оптимизации и не должны
* использоваться, если нет достаточно логики в самих моделях...
*/
public function __construct(User $user)
{
parent::__construct();
$this->user = $user;
}
/**
* просто вернем нового пользователя при создании сценария
*/
public function emptyUser()
{
return $this->user;
}
/**
* сценарий, в котором происходит создание нового пользователя...
* мы проверяем поля ввода и создаем нового пользователя
*/
public function createUser($input)
{
$validation = $this->validator($input, $this->user->creationRules);
if ($validation->fails()) {
return $this->invoke('createUserInvalid', [$validation, $input]);
}
$this->user->create($input);
return $this->invoke('createUserSuccess', [$user]);
}
}
Вы удивлены тем, что здесь делает функция PHPinvoke()
? Она реализована в моем классе BaseScenario и позволяет мне вызывать защищенные функции контроллера. Рефлексия позволяет мне видеть действия, которые не должны иметь маршрутов, привязанных к ним, то есть PHPcreateUserInvalid
не имеет маршрута и является защищенной функцией. Так что вот, у нас есть контроллер фактически без логики, использующий сценарий. В моей следующей статье, я буду рассказывать о том, как держать шаблоны Laravel простыми и понятными с помощью макросов.
Комментарии (1)
Поздравляю, вы изобрели сервисный слой. Но до гексагональной архитектуры ещё не близко.
Валидация должна быть в классе Request, за одно это исключило бы ваше применение рефлексии которое тут не очень уместно.