В сегодняшней статье серии Предметно-ориентированный Laravel мы поглубже погрузимся в прикладной уровень. Основной тенденцией на протяжении всей серии является сохранение кода чистым, лаконичным и управляемым. Эта статья не будет отличаться, поскольку мы рассмотрим, как держать контроллеры чистыми и точными.
Шаблон, который мы будем использовать, называется шаблоном модели представления. Как следует из названия, эти классы являются моделями для ваших файлов представлений; они отвечают за предоставление данных для представления, которые в противном случае поступали бы непосредственно от контроллера или модели домена. Кроме того, они позволяют лучше разделить проблемы и обеспечивают большую гибкость для разработчика.
По сути, модели представлений – это простые классы, которые принимают некоторые данные и превращают их во что-то полезное для отображения в шаблонах. В этой главе я покажу вам основные принципы построения паттерна, мы рассмотрим как они интегрируются в проекты Laravel и, наконец, я покажу вам как мы используем шаблон в одном из наших проектов.
Давайте начнем.
Допустим, у вас есть форма для создания поста в блоге с категорией. Вам понадобится способ заполнить поле выбора в представлении с помощью параметров категории. Контролер должен их обеспечить.
public function create()
{
return view('blog.form', [
'categories' => Category::all(),
]);
}
Приведенный выше пример работает для метода create, но давайте не будем забывать, что мы также должны иметь возможность редактировать существующие сообщения.
public function edit(Post $post)
{
return view('blog.form', [
'post' => $post,
'categories' => Category::all(),
]);
}
Далее следует новое требование к бизнесу: пользователи должны быть ограничены в том, в каких категориях им разрешено публиковать сообщения. Другими словами: выбор категории должен быть ограничен в зависимости от пользователя.
return view('blog.form', [
'categories' => Category::allowedForUser(
current_user()
)->get(),
]);
Этот подход не масштабируется. Вам придется изменить код как в методе create
и edit
, так и в самом методе. Можете ли Вы себе представить, что происходит, когда вам нужно добавить теги к сообщению? Или если есть еще одна специальная форма администратора для создания и редактирования сообщений?
Следующее решение состоит в том, чтобы сама модель post предоставляла категории, например:
class Post extends Model
{
public static function allowedCategories(): Collection
{
return Category::query()
->allowedForUser(current_user())
->get();
}
}
Существует множество причин, почему это плохая идея, хотя это часто случается в проектах Laravel. Давайте сосредоточимся на самой актуальной для нашего случая проблеме: она все еще допускает дублирование.
Скажем, есть новая модель News
, которая также нуждается в таком же выборе категории. Это снова вызывает дублирование, но на уровне модели, а не в контроллерах.
Другой вариант – поместить метод в User
модель. Это имеет наибольший смысл, но также затрудняет техническое обслуживание. Представьте, что мы используем теги, как упоминалось ранее. Они не полагаются на пользователя. Теперь нам нужно получить категории из модели пользователя, а теги откуда-то еще.
Я надеюсь ясно, что использование моделей в качестве поставщиков данных для представлений также не является серебряной пулей.
Таким образом, везде где вы пытаетесь получить категории, всегда получается какое-то дублирование кода. Это делает его менее поддерживаемым и сложным в понимании.
Именно здесь в игру вступают модели представлений. Они инкапсулируют всю эту логику, чтобы ее можно было повторно использовать в разных местах. У них есть одна ответственность и только одна ответственность: обеспечение представления правильными данными.
class PostFormViewModel
{
public function __construct(User $user, Post $post = null)
{
$this->user = $user;
$this->post = $post;
}
public function post(): Post
{
return $this->post ?? new Post();
}
public function categories(): Collection
{
return Category::allowedForUser($this->user)->get();
}
}
Назовем несколько ключевых особенностей такого класса:
- Все зависимости внедряются, это дает наибольшую гибкость внешнему контексту.
- Модель представления предоставляет некоторые методы, которые могут быть использованы представлением.
- Там будет либо новый, либо существующий Post, предоставленный
post
методом, в зависимости от того создаете вы или редактируете сообщение.
Вот как выглядит контроллер:
class PostsController
{
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', compact('viewModel'));
}
public function edit(Post $post)
{
$viewModel = new PostFormViewModel(
current_user(),
$post
);
return view('blog.form', compact('viewModel'));
}
}
И, наконец, он может быть использован в представлении следующим образом:
<input value="{{ $viewModel->post()->title }}" />
<input value="{{ $viewModel->post()->body }}" />
<select>
@foreach ($viewModel->categories() as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
Модели представлений в Laravel
Предыдущий пример показал простой класс с некоторыми методами в качестве нашей модели представления. Этого достаточно, чтобы использовать шаблон, но в рамках проектов Laravel есть еще несколько тонкостей, которые мы можем добавить.
Например, вы можете передать модель представления непосредственно во view
функцию, если модель реализовала Arrayable
.
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', $viewModel);
}
Теперь представление может напрямую использовать свойства модели представления, такие как $post
и $categories
. Предыдущий пример теперь выглядит так:
<input value="{{ $post->title }}" />
<input value="{{ $post->body }}" />
<select>
@foreach ($categories as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
Вы также можете вернуть саму модель представления в виде данных JSON, реализовав Responsable
. Это может быть полезно при сохранении формы с помощью вызова AJAX.
public function update(Request $request, Post $post)
{
// Update the post…
return new PostFormViewModel(
current_user(),
$post
);
}
Вы можете увидеть сходство между моделями представлений и ресурсами Laravel. Помните, что ресурсы сопоставляются один к одному в модели, в то время как модели представления могут предоставлять любые данные, которые они хотят.
В наших проектах мы фактически используем ресурсы и модели представления в сочетании:
class PostViewModel
{
// …
public function values(): array
{
return PostResource::make(
$this->post ?? new Post()
)->resolve();
}
}
Наконец, в этом проекте мы работаем с компонентами форм Vue, которые требуют данных JSON. Мы создали абстракцию, которая предоставляет эти данные JSON вместо объектов или массивов при вызове геттера:
abstract class ViewModel
{
// …
public function __get($name): ?string
{
$name = Str::camel($name);
// Some validation…
$values = $this->{$name}();
if (! is_string($values)) {
return json_encode($values);
}
return $values;
}
}
Вместо вызова методов модели представления мы можем вызвать их свойства и получить JSON обратно.
<select-field
label="{{ __('Post category') }}"
name="post_category_id"
:options="{{ $postViewModel->post_categories }}"
></select-field>
Погоди, а как же ViewComposer?
Возможно здесь есть некоторое дублирование логики композеров в Laravel. Документация Laravel объясняет композеры представления следующим образом:
Композеры представлений – это обратные вызовы или методы классов, вызываемые при визуализации представления. Если у вас есть данные, которые вы хотите привязать к представлению каждый раз, когда это представление визуализируется, композер представлений может помочь вам организовать эту логику в одном месте.
Композер представлений регистрируются следующим образом (пример взят из документации Laravel):
class ViewComposerServiceProvider extends ServiceProvider
{
public function boot()
{
View::composer(
'profile', ProfileComposer::class
);
View::composer('dashboard', function ($view) {
// …
});
}
// …
}
Как видите, мы можем использовать как отдельный класс, так и замыкание для добавления переменных в представление.
Вот как композеры представлений используются в контроллерах.
class ProfileController
{
public function index()
{
return view('profile');
}
}
Вы их видите? Нет, конечно же нет: композеры представлений зарегистрированы где-то в глобальном состоянии и вы не знаете, какие переменные доступны для представления без этого неявного знания.
Теперь я знаю, что это не проблема в небольших проектах. Когда вы единственный разработчик и у вас есть 20 контроллеров и, возможно, 20 композеров представлений, все это умещается в твоей голове.
Но как насчет тех проектов, о которых мы пишем в этой серии статей? Когда вы работаете с несколькими разработчиками, в кодовой базе, которая насчитывает тысячи и тысячи строк кода, все это больше не будет умещаться в вашей голове – конечно, не в таком масштабе. Более того, мы даже не рассматривали ваших коллег и трудности, с которыми они столкнутся индивидуально и в команде!
Вот почему шаблон модели представления является предпочтительным подходом. Из самого контроллера становится ясно, какие переменные доступны для представления. Кроме того, вы можете повторно использовать одну и ту же модель представления для нескольких контекстов.
Еще одно преимущество, о котором вы, возможно, и не подумали …
это то, что мы можем передавать данные в модель представления явно. Если вы хотите использовать параметр маршрута или связанную модель для определения данных, передаваемых в представление, это делается явно.
В заключение: управление глобальным состоянием-это боль в больших приложениях, особенно если вы работаете с несколькими разработчиками в одном проекте. Также помните, если два средства имеют один и тот же конечный результат, это не значит, что они одинаковы!