Campos
Los campos definen el formulario de un Resource (o de una Página). Se declaran dentro de fields(), agrupados en Section:
public function fields(): array
{
return [
Section::make('Content')
->columns(1)
->schema([
Text::make('title')->rules('required', 'string', 'max:255'),
Toggle::make('is_published')->default(false),
]),
Section::make('Advanced')
->collapsed()
->schema([
Time::make('publish_time')->displayFormat('H:i'),
]),
];
}Section::make($label) acepta columns(int) para definir la cuadrícula del formulario y collapsed() para empezar plegada. Cada sección contiene un array de campos.
La clase base Field
Todos los campos heredan de Field y comparten estos métodos:
| Método | Descripción |
|---|---|
label(string $label) | Etiqueta mostrada en el formulario. Por defecto, Str::headline() del nombre del campo. |
rules(string ...$rules) | Reglas de validación de Laravel, tal como se las pasarías a un Validator ('required', 'max:255', 'unique:posts,slug'...). |
helperText(string $text) | Línea corta de ayuda bajo el campo, para comportamiento que la etiqueta sola no deja claro. |
placeholder(string $text) | Texto de placeholder del input. |
default(mixed $value) | Valor con el que el formulario de creación llega rellenado antes de que el usuario lo toque. El tipo depende del campo (bool para Toggle, string para Select...). |
visibleWhen(string $field, string $operator, mixed $value = null) | Muestra el campo solo cuando otro campo cumple una condición. Véase más abajo. |
visibleOn(string ...$operations) | Muestra el campo solo en una operación: create o edit. |
La validación se recoge automáticamente de todos los campos del Resource vía validationRules() (que viene del trait HasSchemaFields, compartido entre Resource y Page), no hace falta declarar ningún FormRequest.
Visible solo al crear o al editar
Text::make('confirmation')
->visibleOn('create')
->rules('required', 'string')Un campo con visibleOn('create') solo aparece en el formulario de creación, y sus reglas de validación solo se aplican allí. El formulario de un Relation manager no distingue entre crear y editar, así que visibleOn() no filtra nada en él.
Visibilidad condicional
Text::make('category_note')
->rules('required', 'string', 'max:255')
->visibleWhen('category', 'equals', 'announcement')El operador es uno de filled, empty, equals o not_equals (equals/not_equals toman un tercer argumento, el valor a comparar), un conjunto pequeño y fijo, no componible (sin and/or, sin anidar). La condición se guarda como datos dentro del schema, no como closure, porque se evalúa dos veces: en el cliente, para que el campo aparezca y desaparezca en vivo mientras se rellena el formulario, y en el servidor, para que un campo required que el usuario nunca ha visto no pueda bloquear nunca el envío, sus reglas se saltan por completo, no solo se vuelven opcionales, cuando su condición es falsa contra los datos enviados.
Un campo dentro de un Repeater queda circunscrito a su propia fila: la condición mira campos hermanos de la misma fila, no el formulario entero, y cada fila se valida independientemente (la fila 1 puede requerir su url mientras la fila 2, con label vacío, no lo requiere).
Repeater::make('links')
->schema([
Text::make('label')->rules('nullable', 'string', 'max:255'),
Text::make('url')
->rules('required', 'url')
->visibleWhen('label', 'filled'),
])Un campo oculto conserva el valor que ya tenga en el cliente (no se borra automáticamente), así que alternar una condición arriba y abajo no hace perder lo que el usuario ya había escrito.
Catálogo de campos
Text
Entrada de una línea.
Text::make('title')->rules('required', 'string', 'max:255')masked() lo convierte en un campo de contraseña, que oculta lo que se escribe y evita que el navegador lo autocomplete:
Text::make('password')->masked()->rules('required', 'string', 'min:12')Textarea
Entrada de texto multilínea. Misma API que Text (solo label()/rules()).
Textarea::make('body')->rules('nullable', 'string')RichEditor
Editor WYSIWYG (Tiptap en el frontend). Sus ~120 KB de JS solo se cargan en los paneles que usan este campo. El contenido se sanitiza en el servidor en cada save() (vía HtmlSanitizer), independientemente de lo que envíe el cliente.
RichEditor::make('content')->rules('nullable', 'string')Toggle
Interruptor booleano. Sin opciones propias más allá de las de la clase base, default(false) es el default() genérico de Field, con false como valor por defecto del propio Toggle si no declaras ningún otro.
Toggle::make('is_published')
->helperText('Published posts are visible on the public index.')
->default(false)Select
Desplegable de una sola opción.
Select::make('category')
->options(['news' => 'News', 'update' => 'Update', 'announcement' => 'Announcement'])
->nullable()
->rules('nullable', 'string')| Método | Descripción |
|---|---|
options(array<string, string> $options) | Parejas valor => etiqueta. |
nullable(bool $nullable = true) | Permite dejarlo vacío en el frontend. |
Las claves pueden ser enteros, y el desplegable respeta el orden en que declaras las opciones.
CheckboxList
Selección múltiple sobre el mismo formato de opciones que Select. Cuando hay más de una opción, muestra una casilla para marcarlas o desmarcarlas todas. Dentro de una Section plegada, la cabecera muestra cuántas casillas hay marcadas.
CheckboxList::make('notify_channels')
->options(['email' => 'Email', 'push' => 'Push', 'sms' => 'SMS'])
->rules('nullable', 'array')Tags
Una lista libre de palabras, que se guarda como un array. El usuario escribe un valor y lo confirma con Enter, coma o punto y coma, y cada valor queda como una etiqueta que se puede quitar. Los duplicados se ignoran.
Tags::make('keywords')
->itemRules('string', 'max:30')
->rules('nullable', 'array')itemRules() son las reglas de cada elemento, y rules() las del array entero. Se puede usar tanto en un formulario de Resource como en el schema() de una acción.
Placeholder
Un campo de solo lectura que muestra un valor dentro del formulario, sin ninguna entrada. El valor sale de los datos del formulario, así que normalmente lo calculas en mutateDataBeforeFill():
Placeholder::make('summary')->label('Summary')Date y Time
Date::make('published_at')->displayFormat('d/m/Y')->rules('nullable', 'date')
Time::make('publish_time')->displayFormat('H:i')->rules('nullable', 'date_format:H:i')| Método | Descripción |
|---|---|
displayFormat(string $format) | Formato de visualización (sintaxis PHP date()). Y-m-d y H:i por defecto, respectivamente. |
Date se renderiza con el DatePicker de Arrel, que además de la fecha admite un mínimo, un máximo y días deshabilitados. Estas opciones son propiedades del componente, no del campo PHP: las usas en tus páginas personalizadas.
File e Image
Image es un File con type() distinto (image en lugar de file), comparten todo el resto de la API.
Image::make('cover_path')
->directory('posts/covers')
->url(fn (Post $post): ?string => $post->cover_path === null ? null : "/storage/{$post->cover_path}")
->rules('nullable', 'image', 'max:4096')| Método | Descripción |
|---|---|
disk(string $disk) | Disco de destino final. local por defecto. |
directory(string $directory) | Subdirectorio dentro del disco. |
url(Closure $callback) | Resuelve un enlace al archivo que el registro ya tiene guardado, para que el formulario de edición muestre un «ver archivo actual». Recibe el modelo y devuelve el enlace, o null si no hay. |
Cómo funciona la subida: el frontend sube el archivo a un directorio temporal (arrel-temp, disco local) en cuanto el usuario lo selecciona, antes de guardar el formulario. Al hacer save(), Arrel mueve el archivo de arrel-temp a {disk}/{directory}/{nombre} y solo entonces escribe la ruta final en la base de datos (véase HasSchemaFields::moveUploadedFiles()). Los temporales que nadie llega a guardar se limpian con php artisan arrel:prune-uploads (véase Instalación).
Repeater
Sub-schema repetible: una lista de grupos de campos con la misma forma.
Repeater::make('links')
->schema([
Text::make('label')->rules('required', 'string', 'max:255'),
Text::make('url')->rules('required', 'url'),
])
->rules('nullable', 'array')schema() toma un array de Field (renderizado recursivamente: un Repeater puede contener otro Repeater). Las reglas de validación de cada subcampo se aplican con la notación de puntos de Laravel para arrays (links.*.label, links.*.url), generada automáticamente, y respetan el visibleWhen() propio de cada fila.
Relation
Un desplegable con búsqueda sobre un modelo relacionado. Tiene dos modos, según si la relación es de un solo valor o de muchos:
Valor único (belongsTo): el campo se llama como la columna de clave foránea, no como el método de relación, para que el Model::create()/update() normal lo trate como cualquier otro atributo.
Relation::make('author_id')
->related(User::class)
->titleAttribute('name')
->searchColumns('name', 'email')
->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at'))
->nullable()
->rules('nullable', 'exists:users,id')Múltiple (belongsToMany, vía ->multiple()): el campo se llama como el método de la relación (tags, es decir $model->tags()), no como una columna, una tabla pivote no tiene clave foránea con la que nombrarlo. Usa ->relationship('nombre') cuando el nombre del campo y el del método deban diferir.
Relation::make('tags')
->multiple()
->related(Tag::class)
->titleAttribute('name')
->rules('array')| Método | Descripción |
|---|---|
related(class-string<Model> $model) | El modelo relacionado. Obligatorio. |
titleAttribute(string $attribute) | Atributo mostrado en cada opción del desplegable. name por defecto. |
searchColumns(string ...$columns) | Columnas contra las que busca el combobox. Por defecto, solo titleAttribute(). |
query(Closure $callback) | Restringe qué registros se pueden elegir. También se aplica al guardar (véase más abajo). |
searchUsing(Closure $callback, ?Closure $refine = null) | Sustituye la búsqueda por defecto del desplegable. Véase «Búsqueda a medida». |
dependsOn(string $field) | Las opciones dependen del valor de otro campo del formulario. Véase «Opciones dependientes». |
nullable(bool = true) | Solo relevante en modo de un solo valor. |
multiple(bool = true) | Cambia el campo a selección múltiple (chips), sobre una relación belongsToMany. |
relationship(string $name) | Nombre del método de relación, cuando difiere del nombre del campo. Por defecto, el mismo nombre del campo. |
itemRules(string ...$rules) | Reglas de validación para cada elemento seleccionado en modo múltiple. Por defecto, integer más un exists:{tabla},{clave} derivado de related(). |
El alcance se aplica al guardar
query() no solo filtra el desplegable: también se aplica cuando se guarda. Un registro que el alcance oculta es rechazado con un error de validación, aunque alguien envíe su identificador a mano. Esto vale para los formularios de un Resource, de un Relation manager y de una acción.
La closure recibe tres argumentos: el query builder, el valor del campo de dependsOn() (o null si no hay), y el registro padre cuando el campo está dentro de un Relation manager o de una acción. Debe mutar el builder que recibe.
Relation::make('room_id')
->related(Room::class)
->query(function (Builder $query, mixed $dependsOn, ?Model $parent): void {
$query->where('school_id', $parent?->school_id);
})Opciones dependientes
dependsOn() hace que las opciones de un campo se vuelvan a cargar cuando cambia otro campo del formulario. El valor de ese campo llega como segundo argumento de la closure de query():
Relation::make('room_id')
->related(Room::class)
->dependsOn('building_id')
->query(function (Builder $query, mixed $buildingId): void {
$query->where('building_id', $buildingId);
})Búsqueda a medida
Por defecto, el desplegable busca con un LIKE sobre searchColumns(). searchUsing() lo sustituye para columnas que no se pueden comparar así, como una cifrada:
Relation::make('student_id')
->related(Student::class)
->searchUsing(
fn (Builder $query, string $term): Builder => $query->where('name_index', 'like', substr($term, 0, 3).'%'),
refine: fn (Student $student, string $term): bool => str_contains(mb_strtolower($student->name), mb_strtolower($term)),
)La primera closure estrecha los candidatos en SQL (hasta 500). La segunda, refine, decide en PHP cuáles de esos candidatos coinciden realmente. Sin refine, el resultado es lo que la primera closure deja en el builder.
Cómo se guarda una relación múltiple: al crear/actualizar el registro, Arrel hace un sync() (reemplazo completo, no una adición) contra la relación, antes de que se ejecuten afterCreate()/afterUpdate(), así que estos hooks siempre ven la relación ya sincronizada. Como sync() reemplaza todo el conjunto de filas pivote, cualquier columna pivote extra se reinicia en las filas resincronizadas, editar datos pivote es trabajo de un Relation manager, no de este campo. Una clave simplemente ausente del payload de la petición nunca se toca, así que una actualización parcial no puede borrar por accidente una relación existente.
Referencia rápida
| Campo | type() | Opciones propias |
|---|---|---|
Text | text | masked() |
Textarea | textarea | Ninguna |
RichEditor | rich_editor | Ninguna |
Toggle | toggle | Ninguna |
Select | select | options(), nullable() |
CheckboxList | checkbox_list | options() |
Tags | tags | itemRules() |
Placeholder | placeholder | Ninguna |
Date | date | displayFormat() |
Time | time | displayFormat() |
File | file | disk(), directory(), url() |
Image | image | disk(), directory(), url() |
Repeater | repeater | schema() |
Relation | relation / relation_multiple | related(), titleAttribute(), searchColumns(), searchUsing(), query(), dependsOn(), nullable(), multiple(), relationship(), itemRules() |
Todos los campos, además, comparten helperText(), placeholder(), default(), visibleWhen() y visibleOn() de la clase base.