Часть 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-подгрузкой значений при первичном осмотре не обнаружено.
Медиум начал подглючивать на длинно-посте,