Знакомство с Laravel Nova

Часть 2

[Часть 1]

Линзы

Попробуем сделать “линзу”.. или “выборку” пользователей, у которых скоро день рождения.

php artisan nova:lens BirthdaySoon

Команда создаст нам папочку Lenses в app\Nova с файлом BirthdaySoon.php он содержит метод query в котором мы должны реализовать запрос, метод fields — где мы описываем поля которые будем выводить, метод filters в котором мы можем задать фильтры для данной линзы, а так же uriKey содержащий часть запроса, по которому линза будет отображаться. Для нашей задачи реализация выходит вот такая

class BirthdaySoon extends Lens
{
private const INTERVAL = 7;

public $name = 'Ближайшие ДР';

/**
* Get the fields available to the lens.
*
*
@param \Illuminate\Http\Request $request
*
*
@return array
*/
public function fields(Request $request)
{
return [
ID::make('ID', 'id')->sortable(),
Gravatar::make('Аватар')
->setSize(150)
->setDefault(Gravatar::MONSTER),
Text::make('Имя', 'name')->sortable(),
Date::make('Дата рождения', 'birthday',
function($value) {
return Carbon::createFromFormat('Y-m-d', $value)->format('d.m.Y');
}),
];
}

/**
* Get the filters available for the lens.
*
*
@param \Illuminate\Http\Request $request
*
*
@return array
*/
public function filters(Request $request)
{
return [];
}

/**
* Get the URI key for the lens.
*
*
@return string
*/
public function uriKey()
{
return 'birthday-soon';
}

/**
* Get the query builder / paginator for the lens.
*
*
@param \Laravel\Nova\Http\Requests\LensRequest $request
*
@param \Illuminate\Database\Eloquent\Builder $query
*
*
@return mixed
*/
public static function query(LensRequest $request, $query)
{
$query->select(['users.id', 'users.name', 'users.email', 'p.birthday'])
->leftJoin('user_profiles as p', 'users.id', '=', 'p.id')
->where(function (Builder $sq) {
$sq->orWhere(function (Builder $q) {
$q->whereMonth('p.birthday', '=', Carbon::now()->month)
->whereDay('p.birthday', '>=', Carbon::now()->day);
})->orWhere(function (Builder $q) {
$q->whereMonth('p.birthday', '=', Carbon::now()->addDays(self::INTERVAL)->month)
->whereDay('p.birthday', '<=', Carbon::now()->addDays(self::INTERVAL)->day);
});
});
if ($request->has('orderBy') && $request->orderBy == 'birthday') {
$query->orderBy(
'p.birthday',
$request->orderByDirection === 'asc' ? 'asc' : 'desc'
);
return $request->withFilters($query);
}
return $request->withOrdering($request->withFilters($query));
}
}

Чтобы добиться сорировки по полю birthday пришлось поковыряться — потому что по умолчанию в методе $request->withOrdering оно подставляет алиас таблицы основного ресурса

Осталось зарегистрировать линзу в ресурсе User — для этого он содержит метод lenses

public function lenses(Request $request)
{
return [
new BirthdaySoon()
];
}

При просмотре ресурса пользователь у нас появилось меню Линзы

При выборе попадаем на страничку

Действия

Попробуем добавить действие для пользователей. Архитектурно предполагается, что действия могут быть обработаны с помощью очереди — например какие-то emai-рассылки. Мы попробуем создать более простое действие — задать роль пользователя.

php artisan nova:action SetUserRole

Создаст нам необходимый кракас, расположенный в директории app/Nova/Actions. Мы запишем свой нехитрый обработчик в методе handle

class SetUserRole extends Action
{

public $name = 'Назначить роль';
/**
* Perform the action on the given models.
*
*
@param \Laravel\Nova\Fields\ActionFields $fields
*
@param \Illuminate\Support\Collection $models
*
*
@return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
$models->each(function (User $model) use ($fields) {
if (!$model->role()->isStaff()) {
$model->update(['role' => $fields->role]);
}
});
}

/**
* Get the fields available on the action.
*
*
@return array
*/
public function fields()
{
return [
Select::make('Роль', 'role')
->options(Role::variants())
->displayUsingLabels(),
];
}
}

И, конечно, подключаем действие к ресурсу.

Сначала подумала, что не сработало, так как страница ресурса пользователей не изменилась. Но стоит только отметить какой-нибудь чекбокс, как появляется меню действий.

При выборе действия возникает модальное окно, в котором предлагается выбрать нужную роль, после завершения всплывает notice что задача выполнена успешно. Так же можно заметить, что при выборе строк чекбоксами появляется корзика с функцией удаления записей

Резюме: Мощный и весьма функциональный инструмент.

Метрики

Nova нам предлагает 3 типа метрик — Оценочная метрика (Value Metric), Метрика прогресса (Trend Metric) и Сравнительная метрика (Partial Metric) . Пока у нас чего-то оценивать и измерять прогесс данных маловато. Попробуем сделать Сравнительную метрику соотношения полов

php artisan nova:partition UsersPerGender

Созданный каркас предлагает нам методы calculate для расчета значений, cacheFor для указания времени кеширования и uriKey для роута

Для нашей задачи метод calculate будет таким

public function calculate(Request $request)
{
return $this
->count($request, UserProfile::class, 'sex')
->label(function ($value) {
return (new Sex($value))->name();
}
});
}

А регистрируются метрики в методе cards ресурса. 
На нашей главной страничке пользователей появилась такая диаграммка

Кстати, чтобы не отображать в меню отдельный ресурс профилей, можно установить переменную в ресурсе

public static $displayInNavigation = false;

На этом пока закончим с ресурсом пользователи, и рассмотрим, что нам предложит Nova для кейса создания страниц с контентом.

Ресурс “Страницы”

Создадим новую миграцию, модель Page и заполним данными

Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->string('slug')->unique();
$table->string('title');
$table->text('body');
$table->string('status')->default('draft');
$table->timestamps();
});

Создадим новый ресурс

art nova:resource PageResource

Как показала практика, лучше классы и модели и называть по-разному.

Для полей ресурса устанавливаю настройки

public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Название', 'title')->rules(['required', 'string', 'max:255']),
Text::make('Алиас', 'slug')->hideFromIndex()
->rules(['required', 'string', 'max:255']),
Trix::make('Содержание', 'body'),
Select::make('Статус', 'status')->options(SaveStatus::variants())
->sortable()->displayUsingLabels(),
DateTime::make('Создано', 'created_at')->format('d.m.Y H:i:s')->sortable()
];
}

Уже немного разобравшись на предыдущих задачах, на создание реусурса, фильтра по статусу, действия на установку статуса и метрику по количеству статей за период ушло не больше 15 минут.

Редактор Трикс выглядит весьма скромненько. Особенно огорчает упомянутое в документации отсутствие интеграции для вставки файлов/изображений

Так же “визуальность” работает не очень хорошо, выделения цитаты и отметки курсивом еле заметно.

Так текст выглядит при просмотре

Маркдаун — редактор выглядит чуть-более проработано

Но вставка медиа-контента только маркдаун- разметкой далеко не всегда айс, вполне сойдёт для кодера и прошаренного юзера, но для контент-редактора уровня менеджера/офисного работника без допила малопригодно. Хочется надеяться, что работа в этом направлении еще будет продолжаться.

А наличие редактора с подсветкой синтаксиса, конечно, очень радует

Кастомизация

Это, пожалуй, наиболее сильная сторона данного продукта, позволяющая обойти немалое количество аналогов. Уже сейчас, спустя совсем небольшое время после релиза, на packagist можно найти немало пакетов — расширений https://packagist.org/?query=laravel-nova

С помощью

php artisan nova:field vendor/fieldname
php artisan nova:card vendor/cardname
php artisan nova:tool vendor/toolname

Позволяют сгенерить скелеты расширений совмещающих как фронт так и бек-части, соответственно для полей, карточек и инструментов (по сути своих специфических страниц, отличающихся от парадигмы стандартного CRUD), что позволяет админке быть по-настоящему гибким решением.

Так же нельзя не отметить предусмотренные возможности проверки прав доступа на каждый элемент.

В общем, не смотря на небольшую сырость, продукт достойный. И наверное, ольше ориентрированный на e-commerce/crm-erp продукты, чем cms

Upd.

Обнаружился еще один серьёзный недостаток, вызвавший немалое удивление

No-Responsive :-(

С респонсивностью у шаблона очень печально

Для желающих пощупать — есть демо (правда на самом первом релизе)
https://laramail.io/nova
логин:admin@laramail.io
пасс:admin12345

По хештегу #laravelnova в твиттере можно быть в курсе последних новостей

Так, анонсировано, что идет разработка над менеджером файлов и менеджером меню