Поля

Основы

Концепция

Полям отводится важнейшая роль в админ-панели MoonShine. Они используются в FormBuilder для построения форм, в TableBuilder для создания таблиц, а также в формировании фильтра для ModelResource (CrudResource). Их можно использовать в ваших кастомных страницах и даже вне админ-панели как в виде объектов, так и непосредственно в Blade.

Создать экземпляр поля очень просто. Для этого есть удобный метод make() и для базового использования достаточно указать label и name поля.

use MoonShine\UI\Fields\Text;
 
Text::make('Title')
Text::make('Title', 'title')

Чаще всего поля используются внутри FormBuilder, где за счет самого FormBuilder они на основе реквеста могут изменять исходный объект.

Режимы работы

Сложность понимания полей MoonShine обусловлена наличием нескольких визуальных состояний.

Режим по умолчанию

Поля являются элементами формы, поэтому их состояние по умолчанию при рендере является просто HTML элементом формы. Например, для поля Text визуальное состояние по умолчанию будет <input type="text" .../>.

Режим Preview

Этот режим служит для отображения значения поля. При выводе поля через TableBuilder, нам не нужно его редактировать, мы просто хотим показать его содержимое. Давайте рассмотрим поле Image, его preview вид будет иметь img миниатюру или карусель изображений если режим multiple.

Тем самым каждое поле выглядит по-разному как и разнообразие элементов формы, но также и выглядят по-разному в режиме preview так как у них разные назначения.

Это имеет преимущество в том, что нам, разработчикам, не нужно беспокоиться о том, как отобразить, например, поле Date. Под капотом MoonShine выполнит форматирование, экранирует текстовые поля для обеспечения безопасности или просто сделает вывод более эстетичным.

Режим Raw

Возможно, при использовании панели, вам и не придётся использовать этот режим. Его суть в том, что он просто выведет значение поля, которое было ему присвоено изначально без дополнительных модификаций.

Режим идеально подходит для экспорта, чтобы в итоге отобразить исходное содержимое для дальнейшего импорта.

Цикл жизни поля

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

Цикл через FormBuilder

  • поле объявлено в ресурсе,
  • поле попадает в FormBuilder,
  • FormBuilder наполняет поле,
  • FormBuilder рендерит поле,
  • при реквесте FormBuilder вызывает поля и сохраняет за счет них исходный объект.

Цикл через TableBuilder

  • поле объявлено в ресурсе,
  • поле попадает в TableBuilder,
  • TableBuilder включает поле в режим Preview,
  • TableBuilder итерирует исходные данные и трансформирует их в TableRow предварительно наполнив каждое поле данными,
  • TableBuilder рендерит себя и каждый свой row вместе с полями.

Цикл через экспорт

  • поле объявлено в ресурсе в методе для экспорта,
  • поле попадает в Handler,
  • Handler включает поле в режим Raw,
  • Handler итерирует исходные данные, наполняет ими поля и генерирует таблицу для экспорта на основе сырых значений полей.

Поля в MoonShine не привязаны к модели (за исключением поля Slug и полей отношений), поэтому спектр их применения ограничивается только вашей фантазией.

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

Изменить preview

Допустим, Вы используете поле Select с опциями в виде ссылок на изображения и хотите в режиме preview выводить не ссылки, а сразу рендерить изображения. Результат можно достигнут за счет метода changePreview(). Ваш код будет выглядеть следующим образом:

 namespaces
use MoonShine\UI\Components\Carousel;
use MoonShine\UI\Fields\Select;
 
Select::make('Links')->options([
'/images/1.png' => 'Picture 1',
'/images/2.png' => 'Picture 2',
])
->multiple() // Поле может иметь несколько значений
->fill(['/images/1.png', '/images/2.png']) // Мы наполнили поле, указали какие значения выбраны
->changePreview(
fn(?array $values, Select $ctx) => Carousel::make($values)
) // изменили состояние preview

В итоге вы получите карусель изображений на основе значений Select, вы можете вернуть компонент или любую строку.

Изменить наполнение

В предыдущем примере мы использовали метод fill() для наполнения поля Select. Но, если мы используем его в готовом ModelResource или FormBuilder, то поле будет наполнено за нас и данные, переданные в метод fill(), будут перезаписаны. В ваших же задачах может возникнуть ситуация, когда вам потребуется изменить логику наполнения, интегрироваться в этот процесс. В этом случае вам помогут методы changeFill() и afterFill().

Давайте рассмотрим всё тот же пример с Select и изображениями, но при этом преобразуем относительный путь в полный URL.

В данном случае наполнение происходит автоматически, эти действия сделает за нас FormBuilder и ModelResource, мы же только изменим процесс.

 namespaces
use MoonShine\UI\Components\Carousel;
use MoonShine\UI\Fields\Select;
 
Select::make('Images')->options([
'/images/1.png' => 'Picture 1',
'/images/2.png' => 'Picture 2',
])
->multiple()
->changeFill(
fn(Article $article, Select $ctx) => $article->images
->map(fn($value) => "https://cutcode.dev$value")
->toArray()
)
->changePreview(
fn(?array $values, Select $ctx) => Carousel::make($values)
),

Данный метод принимает полный объект, который был передан FormBuilder в поля и поскольку мы рассматривали контекст с ModelResource, то исходные данные у нас были Model - Article.

В процессе мы вернули значения, необходимые для поля, но изменив содержимое. Мы использовали changePreview() из предыдущего шага для демонстрации результата.

Рассмотрим ещё один пример наполнения. Допустим, нам нужно при выводе Select в таблицу проверить его значение на определенное условие и добавить класс на ячейку, если оно выполнено. Нам нужно получить итоговое значение, которым наполнен Select, и нам важно, чтобы наполнение обязательно уже произошло (так как условный метод when() вызывается до наполнения и нам не подходит).

 namespaces
use MoonShine\UI\Components\Carousel;
use MoonShine\UI\Fields\Select;
 
Select::make('Links')->options([
'/images/1.png' => 'Picture 1',
'/images/2.png' => 'Picture 2',
])
->multiple()
->afterFill(
function(Select $ctx) {
if(collect($ctx->toValue())->every(fn($value) => str_contains($value, 'cutcode.dev'))) {
return $ctx->customWrapperAttributes(['class' => 'full-url']);
}
 
return $ctx;
}
)
->changePreview(
fn(?array $values, Select $ctx) => Carousel::make($values)
),

Построитель полей обладает широкими возможностями и вы можете менять любые состояния прям на лету. Давайте рассмотрим редкий случай изменения визуального состояния по умолчанию, хотя мы и не рекомендуем этого делать и лучше будет создать отдельный класс поля для этих задач, чтобы вынести логику и переиспользовать поле в дальнейшем. Но всё же представим, что из поля Select по каким-то причинам мы хотим сделать поле Text:

use MoonShine\UI\Fields\Select;
 
Select::make('Links')->options([
'/images/1.png' => 'Picture 1',
'/images/2.png' => 'Picture 2',
])
->multiple()
->changeRender(
fn(?array $values, Select $ctx) => Text::make($ctx->getLabel())->fill(implode(',', $values))
)

Смена режима отображения

Как мы уже поняли, поля имеют разные визуальные состояния: в FormBuilder по умолчанию это будет элемент формы, в TableBuilder различное отображение значения, а скажем в экспорте - просто исходное значение.

Но давайте представим ситуацию, что нам необходимо в TableBuilder вывести поле не в режиме preview, а в режиме по умолчанию или наоборот внутри FormBuilder вывести в preview режиме или вообще в исходном:

Text::make('Title')->defaultMode()

Независимо от того, где мы будем выводить это поле - оно всегда будет в режиме по умолчанию, в виде элемента формы:

Text::make('Title')->previewMode()

То же самое, только всегда будет в режиме preview.

Ну и режим с исходным состоянием напоследок:

Text::make('Title')->rawMode()

Раз уж мы с вами затронули тему rawMode и уже обсуждали процесс изменения наполнения, давайте также взглянем на метод, который позволяет модифицировать исходное значение. Например, мы используем поле для экспорта и нам не нужно выполнять последующий импорт, необходимо отобразить значение для менеджера в понятном формате:

use MoonShine\Laravel\Fields\Relationships\BelongsTo;
 
BelongsTo::make('User')
->modifyRawValue(
fn(int $rawUserId, Article $model, BelongsTo $ctx) => $model->user->name
)

Давайте также представим ситуацию, что вам необходимо делать экспорт в понятном для менеджеров формате, но при этом также в дальнейшем импортировать этот файл. Каким бы не был умным MoonShine, он не поймет, что значение "Иван Иванов" нужно найти в таблице users по полю name и взять только id. Эту задачу можно решить следующим образом:

use MoonShine\Laravel\Fields\Relationships\BelongsTo;
 
BelongsTo::make('User')
->fromRaw(
fn(string $name) => User::where('name', $name)->value('id')
)

Процесс применения полей

Мы уже знаем, что поля работают с любыми данными и это не обязательно Model. Но поля также могут модифицировать поступающие в них данные. По-простому это можно назвать сохранением, но мы не используем этот термин, так как не всегда поля сохраняют. Например, исходными данными может быть QueryBuilder, а поля будут выступать в роли фильтров и тогда они будут модифицировать запрос QueryBuilder или любой другой кейс. Поэтому правильнее будет сказать, что поля "применяются" (apply).

Цикл жизни применения поля (на примере сохранения модели)

  • FormBuilder принимает исходный объект, пусть это будет модель User,
  • итерирует поля, передавая в них объект User и вызывая метод поля apply(),
  • поля внутри apply() берут значение из реквеста на основе своего свойства column,
  • поля на основе своего свойства column модифицируют это поле модели User и возвращают её обратно,
  • после FormBuilder вызовет метод save() модели,
  • также перед apply() методом поля будет вызван метод beforeApply(), если требуется что-нибудь сделать с объектом до основного применения,
  • после метода save() модели у полей будет вызван метод afterApply() (что в данном кейсе хорошо подойдет для полей отношений, чтобы у них был исходный объект который уже сохранен в бд).

Цикл жизни применения поля (на примере фильтрации)

  • FormBuilder принимает исходный объект QueryBuilder,
  • итерирует поля передавая в них объект QueryBuilder и вызывая метод поля apply(),
  • поля на основе своего свойства column модифицируют QueryBuilder и возвращают его обратно,
  • после объект QueryBuilder будет использован при выводе данных.

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

Построитель полей позволяет легко достичь этих целей на лету:

 namespaces
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use MoonShine\UI\Fields\Text;
 
Text::make('Thumbnail by link', 'thumbnail')
->onApply(function(Model $item, $value, Text $field) {
$path = 'thumbnail.jpg';
 
if ($value) {
$item->thumbnail = Storage::put($path, file_get_contents($value));
}
 
return $item;
}
)

Тем самым мы просто добавили ссылку в текстовое поле, но не сохранили её как есть, а загрузили и положили в storage и вернули итоговый путь.

Также нам доступны методы onBeforeApply() и onAfterApply().

Далее давайте взглянем на интерфейс полей более детально, а также на каждое поле отдельно.