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

Часть 1

На днях Taylor Otwell анонсировал новую админ-панель с множеством интересных возможностей, и мне трудно было отказать себе в удовольствии опробовать новинку. В общем, разбираюсь и комментирую по ходу…

Установка

сначала создадим новый laravel-проект

#laravel new laranova
#yarn install

и настроим в конфиге базу данных.

Распаковываем пакет c админкой в папку nova, регистрируем его в composer.json проекта

"require": {
"php": "^7.1.3",
"fideloper/proxy": "^4.0",
"laravel/framework": "5.7.*",
"laravel/tinker": "^1.0",
"laravel/nova": "*"
},
"repositories": [
{
"type": "path",
"url": "./nova"
}
],

и запускаем

#composer update
#php artisan nova:install
#php artisan migrate

У нас установились таблицы laravel по-умолчанию — users и password_resets, а так же новая — action_events

Так же, у нас появились новые файлы и директории

  • public/nova-assets/ содержащая скрипты и стили админки
  • resources/views/vendor/nova/ с шаблонами логотипа в svg и менюшки пользователя
  • app/Providers/NovaServiceProvider.php
  • app/Nova/Resource.php — абстрактный класс ресурса — базового кирпичика коммуникации между Eloquent — моделью и отображением в админке
  • app/Nova/User.php — реализация ресурса для модели пользователя
  • config/nova.php — файл конфигурации, где задано название сайта, вивдимо, отображаемое в панели, базовый url сайта, ‘path’ — часть урла к админке — по умолчанию ‘nova’ и массив middlewares

Создадим через tinker пару пользователей, запустим `php artisan serve` и посмотрим, как выглядит наша админка, перейдя по роуту http://127.0.0.1:8000/nova

Видим форму входа

Сразу решаю поэкспериментировать, поменям значение ‘path’ в config/nova.php на secret и эксперимент оказывается успешным. Админка становится доступна по новому адресу. Жестко забитый роут на админку — лишний минус безопасности.

Следующее, что бросается в глаза — это язык. Гугл подсказывает нам репу с уже готовой локализацией https://github.com/coderello/laravel-nova-lang, ставим звездочку и используем.

И, наконец, входим в админку

Первый экран входа

Резюме: Установка весьма простая, вполне можно поставить на разрабатываемый проект

Ресурс “Пользователи”

Пытаемся ввести что-то в поиске, и получаем всплывашку с ошибкой

Видимо при разработке в основном опирались на mysql, а у меня postgres. Убираем из массива $search в ресурсе User поле ‘id’ и функционал поиска становится работоспособным. (Более правильный вариант — переопределить запрос поиска, реализовав свою функцию в ресурсе User)

/**
* Apply the search query to the query.
*
*
@param \Illuminate\Database\Eloquent\Builder $query
*
@param string $search
*
@return \Illuminate\Database\Eloquent\Builder
*/
protected static function applySearch($query, $search))

(Кстати, запросы можно найти в App\Nova\PerformsQueries)

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

и отмечаем, что с переводом не всё гладко. В ru.json есть запись “No :resource matched the given criteria” с переводом, но почему-то он не подхватывается.

Попробуем русифицировать User на пользователя. Для этого нам понадобится переопреелить в ресурсе методы label и singularLabel

public static function label()
{
return 'Пользователи';
}
public static function singularLabel()
{
return 'Пользователь';
}

С единственным числом выходит не слишком красиво, падежи англоязычными разработчиками не предусмотрены. Видимо надо будет как-то разруливать через локализацию

Играемся с полями. Во-первых, идея граватар конечно, неплоха, но так как email у нас фейковые, запрос по граватр ничего не находит, хотя у них поддерживаются интересные опции, отдавать генерируемые иконки, если ничего не найдено. Но в nova/src/Fields/Gravatar урл генерации прописан без поддержки параметров. И хотя руки тянутся просто прописать доп. параметр прямо в исходник этого поля, мы так делать не будем, иначе в последствии огребем при обновлении релиза. Поэтому создадим в app/Nova папочку своих Fields и сделаем своё поле с блекджеком…

namespace App\Nova\Fields;
use Laravel\Nova\Fields\Gravatar as BaseGravatar;
class Gravatar extends BaseGravatar
{
const MYSTY = 'mp';
const IDENTICON = 'identicon';
const MONSTER = 'monsterid';
const WAVATAR = 'wavatar';
const RETRO = 'retro';
const ROBOHASH = 'robohash';
const BLANK = 'blank';
const DEFAULT = '404';

private $default = 'wavatar';
private $size = 200;

public function setDefault(string $default)
{
$this->default = $default;
return $this;
}

public function setSize(int $size)
{
$this->size = $size;
return $this;
}
/**
* Resolve the given attribute from the given resource.
*
*
@param mixed $resource
*
@param string $attribute
*
*
@return mixed
*/
protected function resolveAttribute($resource, $attribute)
{
$callback = function () use ($resource, $attribute) {
return strtr('https://www.gravatar.com/avatar/{attr}?s={size}&d={default}', [
'{attr}'=> md5($resource->{$attribute}),
'{size}'=> $this->size,
'{default}' =>$this->default
]);
};

$this->preview($callback)->thumbnail($callback);
}
}

В app/Nova/User.php заменим поле Gravatar на наше, и заодно, добавим локализованные названия полей, а так же поле для роли

public function fields(Request $request)
{
return [
ID::make()->sortable(),
Gravatar::make('Аватар')->setSize(150)
->setDefault(Gravatar::MONSTER),
        Text::make('Имя', 'name')->sortable()
->rules('required', 'max:255'),
        Text::make('Мыло', 'email')->sortable()
->rules('required', 'email', 'max:255')
->creationRules('unique:users,email')
->updateRules('unique:users,email,{{resourceId}}'),
        Password::make('Пароль', 'password')
->onlyOnForms()
->creationRules('required', 'string', 'min:6')
->updateRules('nullable', 'string', 'min:6'),
        Select::make('Роль', 'role')
->options(Role::variants())
->displayUsingLabels()
];
}

А в навбаре аватар не появился…

Смотрим resources/vendor/nova/user.blade.php Там ссылка на gravatar прописана вручную — придется поменять

Попробуем создать нового пользователя

И снова отмечаем что с переводом не все гладко. В этом интерфейсе User не локализовался (Как и в интерфейсе для редактирования)

Страница просмотра записи

Добавляем Профиль

Создадим табличку профиля (и соотв. модель UserProfile) и проверим как работать со связью

Schema::create('user_profile', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id')->unique();
$table->string('full_name')->nullable();
$table->string('company', 512)->nullable();
$table->string('mobile', 15)->nullable();
$table->date('birthday')->nullable();
$table->text('about')->nullable();
$table->string('sex', 15)->default('undefined');
$table->foreign('user_id')->references('id')->on('users')
->onDelete('cascade');
});

Для создания нового ресурса выполним

# php artisan nova:resource Profile -m UserProfile

И добавим в него следующие поля

public function fields(Request $request)
{
return [
BelongsTo::make('User','user')->exceptOnForms(),
Text::make('ФИО', 'full_name')->rules('nullable', 'max:255'),
Select::make('Пол', 'sex')->options(Sex::variants())->displayUsingLabels(),
Date::make('Дата рождения', 'birthday')->rules('nullable', 'date'),
Text::make('Телефон', 'mobile')->rules('nullable', 'numeric'),
Text::make('Компания', 'company')->rules('nullable', 'string'),
Textarea::make('О себе', 'about')->rules('nullable', 'string'),
];
}

Далее, нам нужно связать ресурс профиля с пользователем. Согласно доке мы для этого прописываем в ресурсе User жадную загрузку

$with = [‘profile’]; — Мы указываем именно название связи

И добавляем в fields пользователя поле-связь

HasOne::make(‘Profile’, ‘profile’)

Так же я сразу заполняю набором фейковых данных

У нас в меню появился новый ресурс.

Клик по нику активен и переводит на карточку пользователя

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

Резюме: Связь работает :-) Настройки в большей части, достаточно интуитивны и разобравшись, можно создавать все достаточно быстро. Из минусов — явно не очень хорошо учтены специфики языков, так же еще не все видимо в языковые файлы вынесено. В меню длинные строки выглядят не очень хорошо, и скрытие бокового сайдбара не поддерживается. Так же пока непонятно — есть ли возможность локализовать datepicker, или своё поле Date с своим компонентом мутить надоВ общем, без напилиника в любом случае не обойтись. Ну, пока оставим как есть и опробуем другие плюшки…

Фильтры

Попробуем сделать фильтр по роли пользователя

php artisan nova:filter UserRoleFilter

Создаст нам папочку Filters в app/Nova и сгенерирует каркас класса

В нём нам доступны методы public function apply(Request $request, $query, $value) в котором мы должны добавить фильтр в запрос

и public function options(Request $request) где мы можем указать значения для выбора

Реализация выходит вот такая

class UserRoleFilter extends Filter
{
public $name = 'Роль';
/**
* Apply the filter to the given query.
*
*
@param \Illuminate\Http\Request $request
*
@param \Illuminate\Database\Eloquent\Builder $query
*
@param mixed $value
*
*
@return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
return $query->where('role', '=', $value);
}

/**
* Get the filter's available options.
*
*
@param \Illuminate\Http\Request $request
*
*
@return array
*/
public function options(Request $request)
{
return [
'Администратор'=>'admin',
'Разработчик'=>'developer',
'Клиент'=>'client',
'Пользователь'=>'user'
];
}
}

Стоит обратить внимание, что опции выбора в формате обратном привычному — сначала название а потом ключ!.
Осталось только зарегистрировать фильтр в файле ресурса пользователя в методе filters

public function filters(Request $request)
{
return [
new UserRoleFilter()
];
}

Так же вполне себе реализуем фильтр по связанной таблице

class UserGenderFilter extends Filter
{
public $name = 'Пол';
/**
* Apply the filter to the given query.
*
*
@param \Illuminate\Http\Request $request
*
@param \Illuminate\Database\Eloquent\Builder $query
*
@param mixed $value
*
@return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
return $query
->
join('user_profiles', 'users.id', '=', 'user_profiles.user_id')
->where('user_profiles.sex', '=', $value);
}
    /**
* Get the filter's available options.
*
*
@param \Illuminate\Http\Request $request
*
@return array
*/
public function options(Request $request)
{
return [
'Женский'=>'female',
'Мужской'=>'male',
'Не определен'=>'undefined'
];
}
}

Резюме: Фильтры делаются достаточно просто и быстро, но… из коробки выходит только возможность фильтров по предзаданному выбору. Задать какой-то тип поля для фильтра — например по дате, с произвольным вводом или ajax-подгрузкой значений при первичном осмотре не обнаружено.

Медиум начал подглючивать на длинно-посте,

продолжение →тут