Последние полгода наше комьюнити CutCode работает над новой версией нашей open-source админ-панели MoonShine. И вот недавно состоялся релиз MoonShine 2. Давайте пройдемся по всем значимым изменениям! Конечно, в одной статье я не смогу осветить все нововведения, но попробую сделать это по-максимуму. Ну а также расскажу о ближайших планах на MoonShine 3.
Новые требования
Laravel >= 10.20
PHP >= 8.1
Новый подход
Если смотреть на MoonShine 2 чисто визуально, то отличий не так и много. Да, у нас немного изменились некоторые поля и компоненты, появилось верхнее меню, но все это мелочь по сравнению с изменениями под капотом - там мы переписали 90% кода. В MoonShine 2 полностью новое ядро, которое дает невероятное количество дополнительных возможностей для разработчиков.
Ресурсы
Ресурсы уже не будут прежними. Теперь базовый ресурс вообще ничего не знает о Eloquent моделях и может работать с любым другим хранилищем.
Мы реализовали ресурс для моделей и называется он ModelResource. Те, кто использовал MoonShine 1, даже не увидят разницы в подходе. Но здесь нужно обратить внимание на то, что ресурс теперь изолирован и вы можете написать реализацию своего хранилища: будь то данные из внешнего api или парсинг лог файлов. Тут уже вы ограничиваетесь только своей фантазией.
Страницы
Новый постоянный житель MoonShine. Теперь страницы это основа MoonShine. Да у нас уже были кастомные страницы (которых к слову теперь нет), но текущие страницы не имеют границ и могут работать даже без ресурса. Ответственность у страниц это отображение компонентов, которых может быть сколько угодно - они могут быть компонентами MoonShine, либо просто blade компонентами или даже livewire! В итоге можно сказать, что ресурс это бокс с общей логикой для набора страниц. Но и опять-таки страницы могут существовать и без ресурса. Профиль пользователя это просто MoonShine-страница, которую можно заменить на свою, тоже самое касается и дашборда (главной страницы)
public function components(): array
{
return [
FormBuilder::make()->fields([
Block::make([
Grid::make([
Column::make([
Heading::make('Text'),
ID::make(),
Hidden::make('Hidden'),
])->columnSpan(6),
Column::make([
Heading::make('Textarea'),
Textarea::make('Textarea'),
TinyMce::make('TinyMce'),
])->columnSpan(6),
]),
LineBreak::make(),
]),
])->submit('Submit', ['class' => 'btn-lg btn-primary']),
];
}
Слои
Как я уже говорил ранее, для вашего удобства мы уже написали ресурс для моделей - ведь все-таки мы используем Laravel, и я думаю в 99% случаев ресурсы будут на основе моделей. Также к ресурсу мы добавили готовые страницы:
для листинга записей (IndexPage),
добавления/редактирования(FormPage) ,
детальная(DetailPage).
Чтобы вам удобно было располагать и наполнять компоненты на этих страницах, мы добавили подход с использованием слоев (Layers). В итоге страницы выглядят следующим образом:
public function components(): array
{
return array_merge(
$this->topLayer(),
$this->mainLayer(),
$this->bottomLayer(),
);
}
Теперь, для того чтобы добавить компоненты сверху, достаточно переопределить метод topLayer.
public function topLayer(): array
{
return [
Flex::make([
Heading::make('Title')
])
->customAttributes(['class' => 'mb-4'])
->justifyAlign('end')
];
}
Поля
Всё что нужно знать сейчас о полях, это то, что они также изолированы от модели и могут работать и без нее. Более подробно мы обсудим поля чуть ниже, когда будем рассматривать новые фичи.
Компоненты
Компоненты теперь сердце MoonShine. Всё что вы видите в MoonShine сделано компонентами. Вы можете их добавлять, перемещать, добавлять свои. За счет компонентов мы получили полноценный конструктор, и теперь процесс работы с админ-панелью напоминает сборку кубиков конструктора LEGO - очень увлекательный процесс! Ну и само собой, появилось много новых компонентов и декораций из коробки, о которых вы узнаете в документации.
LayoutBuilder
Изменять структуру основного шаблона теперь проще некуда. Убирайте компоненты, добавляйте свои, используйте декорации - буквально рисуйте шаблон по-своему! Давайте рассмотрим пример, где мы убираем боковую панель и подключаем верхнее меню:
final class MoonShineLayout implements MoonShineLayoutContract
{
public static function build(): LayoutBuilder
{
return LayoutBuilder::make([
Sidebar::make([
Menu::make()->customAttributes(['class' => 'mt-2']),
]),
LayoutBlock::make([
Flash::make(),
Header::make(),
Content::make(),
Footer::make()->copyright(fn (): string => <<<'HTML'
© 2021-2023 Made with ❤️ by
<a href="https://cutcode.dev"
class="font-semibold text-primary hover:text-secondary"
target="_blank"
>
CutCode
a>
HTML)->menu([
'https://moonshine.cutcode.dev' => 'Documentation',
]),
])->customAttributes(['class' => 'layout-page']),
]);
}
}
ActionButtons
Те, кто уже давно используют MoonShine, помнят, что у нас для кнопок в таблице были ItemAction, для массовых действий BulkAction, а в форме FormAction, а на детальной странице DetailAction, ах да еще общие Action для главной страницы сверху. Чуть сам не запутался пока писал) Но все это в прошлом! Встречайте - теперь кнопками правит ActionButton и эти красавцы умеют гораздо больше, чем толпа предыдущих сущностей вместе взятых.
ActionButton::make('Create', $resource->route('crud.create'));
Нужно вызвать модалку по клику, с любым содержимым или подтверждением действий? Не проблема, для этого есть метод - inModal или withConfirm.
ActionButton::make('Create', $resource->route('crud.create'))
->inModal(fn() => 'Create', FormBuilder::make()),
Хотите открыть offcanvas - пожалуйста, воспользуйтесь методом inOffCanvas.
ActionButton::make('Filtes', '#')
->secondary()
->icon('heroicons.outline.adjustments-horizontal')
->inOffCanvas(
fn (): array|string|null => __('moonshine::ui.filters'),
fn (): FormBuilder => new FiltersForm()
)
Кнопка может переходить на любой урл, а также можно получить асинхронно контент или загрузить блейд фрагмент (об этом немного позже), а самое важное что рендерятся они где угодно в MoonShine и есть хелперы для вызова в блейд.
actionBtn('Create', route('example.url'))
->inModal(fn() => 'Create', async: true),
Думаю есть те кто будет ругать js в php классе, но такая возможность присутствует и стоит о ней сказать
ActionButton::make('Create', $resource->route('crud.create'))
->onClick(fn() => 'alert()', 'prevent'),
FormBuilder
Прежде чем мы поговорим о новых возможностях полей, стоит заметить, что изменена их концепция. В MoonShine 1 поля были привязаны к ресурсу и их тяжело было применять за его пределами. Теперь поля можно рендерить отдельно где угодно, но есть и места где им самое место - и это конечно же форма! С приходом MoonShine 2 мы рады представить FormBuilder, c помощью которого вы можете легко наполнить форму полями и декорациями, указать какой тип данных будет у полей (TypeCasts, подробнее в документации), а также использовать Precognition или асинхронное сохранение.
TableBuilder
Еще одно важное место для хранения полей - TableBuilder. Теперь создать таблицу в MoonShine или за её пределами - не проблема. Наполняем её любыми данными и указываем тип данных с помощью TypeCasts. Как и с формами, поддерживается асинхронный режим. Появился инструмент для управления атрибутами ячеек и строк, теперь добавлять им классы, стили, да всё что угодно - можно через ComponentAttributeBag.
Fields
Как бы там ни было, но поля - это основа MoonShine, без них точно никуда. И они всюду. Давайте разберем, что появилось нового! Для начала еще раз повторюсь, что поля ничего не знают о моделях (за исключением полей отношений, тут уж никуда без моделей) и могут наполняться любыми данными, даже за пределами MoonShine. Также появились новые поля, о которых можно почитать подробнее в документации.
Помните фильтры? Ну так вот - их больше не существует и теперь поля могут быть фильтрами, а логику фильтрации вы сможете менять “на лету”.
Теперь можно получать доступ к вложенным элементам через точку (‘user.email’):
Preview::make('Email', 'user.email');
Также логику можно выносить в отдельные Apply-классы, и регистрировать в системе. О Apply классах и MoonShineRegister подробнее смотрите в документации.
Поле Enum прокачали и добавили возможность указывать заголовок и цвет бейджа прямо в Enum, достаточно просто использовать методы getColor/toString.
enum ColorEnum: string
{
case Red = "R";
case Black = "B";
case White = "W";
public function getColor(): string
{
return match ($this->value) {
"R" => 'purple',
"B" => 'gray',
"W" => 'green'
};
}
public function toString(): string
{
return match ($this->value) {
"R" => 'Purple',
"B" => 'Gray',
"W" => 'Green'
};
}
}
BelongsToMany теперь работает со всеми типами полей для pivot значений.
BelongsToMany::make('Categories', 'categories', resource: new CategoryResource())
->fields(function () {
return [
Date::make('Created at')->format('d.m.Y'),
];
})
Async
Select можно указать url и получать опции селекта асинхронно, достаточно только соблюдать структуру ответа, которую мы укажем в документации.
Select::make('Select')
->async(route('async-search'))
->searchable(),
UpdateOnPreview
Отобразить поля теперь можно в таблице в виде элемента формы и асинхронно сохранять при изменении.
Text::make('Title')
->updateOnPreview(fn ($item) => $this->route('resource.update-column', $item->getKey()))
beforeRender/afterRender/changePreview/addAssets/onApply/onBeforeApple/onAfterApple/onAfterDelete
Мы улучшили интерфейс полей и добавили больше возможностей кастомизации без создания отдельного класса. Вот как это работает:
Text::make('Thumbnail')
// Добавили дополнительные скрипты и стили для поля
->addAssets(['custom.js', 'custom.css'])
// В форме до элемента отобразили превью с картинкой
->beforeRender(function (Text $field) {
return $field->preview();
})
// Изменили превью, вместо текста отображаем изображение
->changePreview(function ($value) {
return view('moonshine::ui.image', [
'value' => $value ? Storage::url($value) : null,
]);
})
// Изменили логику сохранения, где мы не просто сохраняем текст а загружаем изображение по указанному урл
->onApply(function (Article $item, string $value) {
$path = 'thumbnail.jpg';
if ($value && $value !== $path) {
Storage::put($path, file_get_contents($value));
$item->thumbnail = $path;
}
return $item;
}),
Badge/link
Добавить ссылку к полю или обернуть его в badge теперь можно для всех текстовых полей
Email::make('Title')
->badge('purple')
->link(fn($value) => "mailto:$value", 'Go to'),
Json
Поле прокачали по-полной, принимает любые типы полей и даже может работать как отношение, отображая форму или таблицу прямо в основной форме
Json::make('Comments')
->asRelation(new CommentResource())
->fields([
ID::make(),
BelongsTo::make('Article')
->setColumn('article_id')
->searchable(),
BelongsTo::make('User')
->setColumn('user_id'),
Text::make('Text')->required(),
Image::make('Files')
->multiple()
->removable()
->disk('public')
->dir('comments'),
])
->creatable()
->removable()
Resource
Еще раз напомню, что в MoonShine 2 ресурс теперь может работать и не на основе моделей. Но все-таки ресурс с моделями у нас прямо в коробке. И он очень похож на тот, который мы использовали в первой версии. Но также появились и новые подходы, а именно:
Раздельный подход, где у нас под каждый тип полей (для главной, формы, детальной страницы) свои отдельные методы.
С публикацией всех страниц ресурса, которые вы сможете кастомизировать по-своему.
Полностью пустой ресурс для нетривиальных задач.
Иконку можно менять прямо в ресурсе через атрибут
#[Icon('heroicons.users')]
class ArticleResource extends ModelResource
Был прокачан поиск по ресурсу и по json, доступ к отношениям и многое другое (смотрим документацию).
Basic
Fragments
Появилась новая декорация Fragment, которая дает возможность использовать blade fragments, отмечать блоки на странице и подгружать их асинхронно.
Fragment::make([
TableBuilder::make(items: $this->getResource()->paginate())
->fields($this->getResource()->getIndexFields())
->buttons([
...$this->getResource()->getIndexButtons(),
]),
])->withName('crud-table'),
Generics
Мы стараемся улучшать интерфейс и внедрили аннотации с дженериками для нашего с вами удобства.
Произвольные правила авторизации
Будет полезно для тех, кто пишет пакеты для MoonShine. Теперь можно регистрировать собственные правила валидации и все они будут применяться в ресурсе.
public function boot(): void
{
MoonShine::defineAuthorization(
static function (ResourceContract $resource, Model $user, string $ability, Model $item): bool {
$hasUserPermissions = in_array(
HasMoonShinePermissions::class,
class_uses_recursive($user),
true
);
if (! $hasUserPermissions) {
return true;
}
if (! $user->moonshineUserPermission) {
return true;
}
return isset($user->moonshineUserPermission->permissions[$resource::class][$ability]);
}
);
}
Handlers
Удобные классы для реализации логики в MoonShine.
class ExportHandler extends Handler
{
public function handle(): Response
{
// Logic
}
}
Красота и сахар
Теперь прямо из коробки доступен компонент с верхним меню вместо левого сайдбара.
return LayoutBuilder::make([
TopBar::make([
Menu::make()->top(),
]),
// ...
]);
Кастомизация темы и цветовой схемы. Фон, кнопки и другие элементы запросто можно перекрасить.
protected function theme(): array
{
return [
'colors' => [
'body' => '#fff'
]
];
}
Директива с ассетами MoonShine, чтобы можно было использовать возможности MoonShine за её пределами.
<head>
@moonShineAssets
head>
Sortable прямо в коробке и уже интегрированы в поля Json/Image/File
x-data="sortable"
Helpers для тех кому нравятся хелперы
moonshine()
moonshineRegister()
to_page()
moonshineRequest()
moonshineAssets()
moonshineMenu()
moonshineLayout()
form()
table()
actionBtn()
// ...
Toasts improvements
Toasts можно вызвать в js.
Stubs для всего
Да кстати, мы интегрировали Laravel Prompts во все консольные команды. Понять структуру кастомных полей или компонентов для разработчиков теперь будет куда проще. Да и установка MoonShine теперь сводится к одной команде.
php artisan moonshine:field
php artisan moonshine:component
php artisan moonshine:apply
php artisan moonshine:controller
php artisan moonshine:layout
php artisan moonshine:page
Новый домен
Проект разрастается и переехал на новый домен - https://moonshine-laravel.com
План на 3.0
Интеграция Laravel Echo, наконец-таки вебсокеты
Multi tenancy
Бесконечная вложенность в меню
Асинхронное удаление полей и записей
Новые шаблоны и темы
2FA
Вложенные ресурсы
Заключение
Обновление получилось очень большим и трудозатратным. И я рассказал только о самых больших изменениях. Подробнее можно посмотреть в обзорном видео:
Работая по MoonShine v.2 мы реализовали много пожеланий участников нашего комьюнити, а некоторые разработчики втягиваются в разработку, реализовывая свои же предложения. Всех Laravel-разработчиков приглашаю присоединиться - использовать MoonShine2 в своих проектах и участвовать в open source разработке.
Данил Щуцкий, автор проекта CutCode.