В противовес серии статей Предметно-ориентированный Laravel хотелось бы показать и иной подход к архитектуре – модельно-ориентированного Laravel.
Laravel – это веб-платформа MVC, обеспечивающая прочную основу для быстрого и легкого запуска веб-приложений. Если вы провели какое-то время в сообществе Laravel, вы, вероятно, знаете, что фреймворк предназначен для предоставления логики приложения, ориентированной на контроллер, а это означает, что большинство процессов вашего приложения реализуются непосредственно внутри классов контроллеров.
Например, возьмите документацию к системе Laravel по валидации. Вы увидите, что логика валидации данных предназначена для использования внутри контроллера с входящим Request
объектом. Аналогично, смотрите, как встроенная система хранения файлов позволяет передавать загруженные файлы непосредственно с контроллера. Это всего лишь пара примеров, но вы можете продолжить чтение документации Laravel, чтобы найти еще много примеров того, как фреймворк буквально разработан для логики, ориентированной на контроллер.
Альтернатива
После многих месяцев экспериментов и совершенствования этой идеи, я решил предложить вам рассмотреть модель-ориентированный подход к структуре Laravel. Конечно, маловероятно, что этот подход будет официально включен в саму структуру, но скорее я предлагаю методику использования структуры, которая расходится с нынешним стандартом.
Почему?
Независимо от того, новичок ли вы в фреймворке или ветеран Laravel, вы можете усомниться в ценности этого подхода. На самом деле, модель-ориентированный подход имеет несколько преимуществ, наиболее существенным из которых является то, что он централизует код в многоразовые функции модели и уменьшает потребность в шаблонной форме в контроллере.
Организация логики в методах модели модулирует логику вашего приложения, что облегчает повторное использование между веб-контроллерами и API, фоновыми заданиями, сидерами и даже Тинкером, вместо копирования ее каждый раз, когда это необходимо. Другим важным преимуществом является то, что этот подход позволяет разрабатывать значительный кусок логики с помощью “конфигурации”, просто включая трейты и устанавливая переменные модели. Фактически, именно так Model
уже функционирует – базовый класс с переменными для настройки поведения модели, такими как $hidden
, $fillable
, $dates
, $dateFormat
, и т. д.
Начнем
Прежде чем я начну, я хотел бы упомянуть способы, которые Laravel уже предлагает в качестве альтернативных подходов, включая некоторые конкретные компоненты, которые уже ориентированы на модель:
Во-первых, большинство компонентов, ориентированных на контроллер, таких как система валидации, также предоставляют методы для ручного использования вне контроллера.
Во-вторых, существует ряд существующих компонентов Laravel, которые непосредственно улучшают ваши модели, включая функцию Laravel Cashier Billable
, которая предоставляет методы для создания, связывания и удаления способов оплаты.
Наконец, сам класс модели включает в себя функциональные возможности для расширения и настройки, такие как средства доступа к атрибутам и мутаторы, boot
функция и возможность загрузки трейтов.
Техника и примеры
Основной фишкой подхода, ориентированного на модель, является включение в класс повторно используемых трейтов и настраиваемых переменных $fillable
, а также других параметров конфигурации базового класса модели.
Все трейты, используемые в следующих примерах, можно найти в репозитории Helpers Helpers Helpers Laravel и установить в свой проект с помощью этого репозитория
composer require helium/laravel-helpers
Пожалуйста, Также обратите внимание, что этот пакет немного грязный, так как он является продуктом месяцев случайных экспериментов. В ближайшем будущем мы планируем переписать все включенные компоненты в виде официальных, хорошо обслуживаемых пакетов.
А также, пожалуйста, помните, что это не учебник по использованию
helium/laravel-helpers
пакета, а скорее обсуждение общей техники. Мы опубликуем официальные учебные пособия по каждому компоненту, как только заново построим пакет.
Пример 1: Номера телефонов моделей
Иногда в моделях может присутствовать поле номера телефона phone_number
. Для такого поля часто требуется приводить его к каком то либо формату при выводе в интерфейсе или для передачи по апи, или для записи в бд. Часто эту задачу решают на местах, каждый раз написав заново или скопировав код форматирования. Хотелось бы как то гарантировать нам, что наш формат везде будет един и стандартизирован во всех наших местах приложения.
На нашем опыте, самый простой способ стандартизировать форматирование, это удалить все спец символы и оставить только цифры. А затем только форматировать его при отображении.
Вот так выглядит подход, ориентированный на контролер:
class UserController extends Controller
{
protected function stripPhoneFormatting(array $data) {
$data['phone_number'] = preg_replace(
'/[^0-9]/',
'',
$data['phone_number']
);
return $data;
}
public function store(Request $request) {
$data = $this->stripPhoneFormatting($request->all());
return User::create($data);
}
public function update(User $user, Request $request) {
$data = $this->stripPhoneFormatting($request->all());
$user->update($data);
return $user;
}
}
Теперь рассмотрим метод, ориентированный на модель:
class User extends Model
{
use HasPhoneNumbers;
}
class UserController extends Controller
{
public function store(Request $request)
{
return User::create($request->all());
}
public function update(User $user, Request $request)
{
$user->update($request->all());
return $user;
}
}
С помощью трейта HasPhoneNumbers
поле phone_number
автоматически очищается от всех специальных символов при установке, без необходимости писать и применять вспомогательную функцию каждый раз, когда вам нужно обновить модель.
Для более расширенной конфигурации, такой как настраиваемые поля номера телефона, добавьте массив phoneNumbers
в свойствах модели:
class User extends Model
{
use HasPhoneNumbers;
protected $phoneNumbers = [
'home_phone_number',
'cell_phone_number',
'office_phone_number'
];
}
Сделаю небольшое отступление от оригинала статьи. Вставлю свои 5 копеек.
Я считаю, что трейты зло и грязь в коде. Можно сделать гораздо проще и красивее. Все что нам понадобится это понимание структур данных.
У нас уже имеются простые типы/структуры, такие как integer, string, boolean. Имеем мы и более сложные – \DateTime. Сложные структуры данных, это обычные классы, которые знают и умеют работать со своим набором данных, например, номером телефона. Приведу пример.
<?php declare(strict_types=1); namespace App\DTO\VO; class Phone { private string $phone; public function __construct(string $phone) { $this->phone = $phone; } public function __toString() { return $this->getFormated(); } public function getFormated(): string { // здесь можно воспользоваться любой библиотекой для форматирования, либо написать свою реализацию форматирования номера телефона return $this->phone; } }
Класс выше минимально описывает структуру сложного типа данных / структуру данных. В моделях, в которых есть поле номера телефона, делается геттер и сеттер, например:
public function getPhone(): ?Phone
{
return $this->phone ? new Phone($this->phone) : null;
}
public function setPhone(Phone $phone)
{
$this->phone = $phone;
}
Далее пользуетесь ими для работы.
$model = new Model;
$model->setPhone('523452354'); // fatal error. несоответствие типов. т.е. мы не сможем записать ничего иного, кроме как наш объект телефона
$model->setPhone(new Phone('523452354')); // all ok
echo $model->getPhone(); // echo formatted phone
Можно пойти еще дальше и создать свои собственные касты
<?php
namespace App\Casts;
use App\DTO\VO\Phone;
class PhoneCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Phone($value);
}
public function set($model, $key, $value, $attributes)
{
return new Phone($value);
}
}
И далее вам потребуется лишь подключить нужный каст в Laravel модели
class User extends Model
{
protected $casts = [
'is_admin' => 'boolean',
'profile_data' => 'array',
'phone' => \App\Casts\PhoneCast::class,
];
}
...
dd(auth::user()->phone) // app\DTO\VO\Phone
Не бойтесь разбивать код на более мелкие абстракции. Не бойтесь создавать тысячи мелких классов/файлов под каждую задачу. Советую посмотреть другие статьи на сайте о том, как сделать код чище и проще для понимания.Например, Предметно-ориентированный Laravel
Пример 2: Самовалидация модели
Теперь давайте рассмотрим пример проверки атрибутов данных вашей модели. Традиционно это обрабатывается в контроллере:
class UserController extends Controller
{
public function store(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|email',
'age' => 'nullable|integer'
]);
return User::create($validatedData);
}
public function update(User $user, Request $request)
{
$validatedData = $request->validate([
'name' => 'sometimes|required|string',
'username' => 'sometimes|required|string',
'age' => 'sometimes|required|integer'
]);
$user->update($validatedData);
return $user;
}
}
Теперь рассмотрим подход, ориентированный на модель:
class User extends Model
{
use SelfValidates;
protected $validatesOnSave = true;
protected $validationRules = [
'name' => 'required|string',
'email' => 'required|email',
'age' => 'required|integer'
];
}
class UserController extends Controller
{
public function store(Request $request)
{
return User::create($request->all());
}
public function update(User $user, Request $request)
{
$user->update($request->all());
return $user;
}
}
Этот пример инкапсулирует логику, которая фактически выполняет проверку модели. Короче говоря, непосредственно перед сохранением модели (т. е.,create
,update
, или save
) внутренний массив $attributes
проверяется на соответствие определенному вами набору правил. Это не только снова уменьшает необходимость воспроизводить логику при каждом обновлении модели, но и защищает модель от ошибочных обновлений, которые применяются вручную после первоначальной проверки контроллера, поскольку она проверяется в последнем возможном экземпляре перед сохранением в базе данных.
Для более сложной конфигурации, такой как условная проверка, просто переопределите метод validationRules
.
class User extends Model
{
use SelfValidates;
protected $validatesOnSave = true;
protected function validationRules()
{
$rules = [
'user_type' => [
'required',
Rule::in(['admin', 'user'])
],
'name' => 'required|string',
'email' => 'required|email',
'age' => 'required|integer'
];
if ($this->user_type = 'admin') {
$rules['admin_group_id'] =
'required|exists:admin_groups,id';
}
return $rules;
}
}
Опять же немного отойду от оригинала статьи. Очень советую ознакомиться с прошлой статьей Автоматическая проверка моделей в Laravel, в которой подробно описываются все минусы данного подхода.
Пример 3: Объединение требований
Этот последний пункт – не столько демонстрация, сколько дискуссия.
Два предыдущих примера могут показаться не имеющими каких-либо существенных преимуществ в простоте по сравнению с традиционным методом, ориентированным на контроллер, по крайней мере, просто глядя на количество строк кода. Однако рассмотрите возможность объединения двух предыдущих примеров в одну User
модель и контроллер, а затем добавьте другие реальные требования, такие как загрузка файлов S3. Как вы можете себе представить, код на стороне контроллера может начать немного раздуваться и становиться очень повторяющимся.
Теперь взгляните на модель-ориентированную перспективу. Конечно, есть немного конфигурации, которую вы должны будете включить в каждый класс модели, но помните, что вам нужно написать конфигурацию только один раз для каждой модели. Кроме того, все, что вам нужно сделать, это обрабатывать запись переменных конфигурации каждый раз, а не переписывать одну и ту же логику каждый раз, когда это необходимо, так как логика реализуется один раз в трейте и используется повторно.
Последнее преимущество заключается в том, что нет никакого риска, что вы или другой разработчик забудете включить определенный бит логики при добавлении новых конечных точек в ваше приложение. Будь то фоновая работа, контроллеры или сидеры, вы почти всегда захотите убедиться, что соблюдаются одни и те же требования. Модель-ориентированный подход гарантирует, что правила всегда применяются последовательно по всем направлениям, и устраняет требование к разработчику быть активно осведомленным обо всех требованиях в любое время.
Вывод
Ориентированный на модель Laravel был моим основным режимом разработки в течение последних нескольких месяцев, и я никогда не вернусь назад! Примеры, приведенные здесь, – это лишь небольшая выборка идей, которые вы можете выполнить с помощью ориентированной на модель прикладной логики, и опять же, вы можете найти целый ряд других черт, которые я построил на репозитории Helpers Helpers Helpers Laravel.
Учитывая размер и разнообразие сообщества Laravel, я надеюсь, что этот альтернативный взгляд на фреймворк вдохновит других исследовать и проверять границы с тем, что вы можете с ним сделать! Как и в жизни, легко застрять в одном образе мышления, не понимая, что еще там есть, поэтому вы должны экспериментировать, играть и смотреть, что возможно. И если кому-то придет в голову интересная идея, обязательно поделитесь ею и продолжите эту дискуссию на благо всех членов этого сообщества!