MoonShine 2.0. Что нового?

Последние полгода наше комьюнити 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.

4
Начать дискуссию