Champs
Les champs définissent le formulaire d'un Resource (ou d'une Page). Ils se déclarent dans fields(), regroupés 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) accepte columns(int) pour définir la grille du formulaire et collapsed() pour commencer repliée. Chaque section contient un tableau de champs.
La classe de base Field
Tous les champs héritent de Field et partagent ces méthodes :
| Méthode | Description |
|---|---|
label(string $label) | Libellé affiché dans le formulaire. Par défaut, Str::headline() du nom du champ. |
rules(string ...$rules) | Règles de validation de Laravel, telles que vous les passeriez à un Validator ('required', 'max:255', 'unique:posts,slug'...). |
helperText(string $text) | Courte ligne d'aide sous le champ, pour un comportement que le libellé seul ne rend pas clair. |
placeholder(string $text) | Texte de placeholder de l'input. |
default(mixed $value) | Valeur avec laquelle le formulaire de création arrive rempli avant que l'utilisateur y touche. Le type dépend du champ (bool pour Toggle, string pour Select...). |
visibleWhen(string $field, string $operator, mixed $value = null) | Affiche le champ uniquement quand un autre champ remplit une condition. Voir plus bas. |
visibleOn(string ...$operations) | Affiche le champ uniquement dans une opération : create ou edit. |
La validation est collectée automatiquement depuis tous les champs du Resource via validationRules() (qui vient du trait HasSchemaFields, partagé entre Resource et Page), il n'y a donc aucun FormRequest à déclarer.
Visible uniquement à la création ou à l'édition
Text::make('confirmation')
->visibleOn('create')
->rules('required', 'string')Un champ avec visibleOn('create') n'apparaît que dans le formulaire de création, et ses règles de validation ne s'y appliquent que là. Le formulaire d'un Relation manager ne distingue pas création et édition, visibleOn() n'y filtre donc rien.
Visibilité conditionnelle
Text::make('category_note')
->rules('required', 'string', 'max:255')
->visibleWhen('category', 'equals', 'announcement')L'opérateur est l'un de filled, empty, equals ou not_equals (equals/not_equals prennent un troisième argument, la valeur à comparer), un ensemble petit et fixe, non composable (pas de and/or, pas d'imbrication). La condition est stockée sous forme de données dans le schema, pas comme closure, parce qu'elle est évaluée deux fois : côté client, pour que le champ apparaisse et disparaisse en direct pendant la saisie du formulaire, et côté serveur, pour qu'un champ required que l'utilisateur n'a jamais vu ne puisse jamais bloquer l'envoi, ses règles sont ignorées complètement, pas seulement rendues facultatives, quand sa condition est fausse par rapport aux données envoyées.
Un champ dans un Repeater est circonscrit à sa propre ligne : la condition regarde les champs voisins de la même ligne, pas le formulaire entier, et chaque ligne est validée indépendamment (la ligne 1 peut exiger son url tandis que la ligne 2, avec un label vide, ne l'exige pas).
Repeater::make('links')
->schema([
Text::make('label')->rules('nullable', 'string', 'max:255'),
Text::make('url')
->rules('required', 'url')
->visibleWhen('label', 'filled'),
])Un champ masqué conserve la valeur qu'il a déjà côté client (elle n'est pas effacée automatiquement), donc basculer une condition d'avant en arrière ne fait pas perdre ce que l'utilisateur avait déjà écrit.
Catalogue de champs
Text
Saisie sur une ligne.
Text::make('title')->rules('required', 'string', 'max:255')masked() le transforme en champ de mot de passe, qui masque ce qui est saisi et empêche le navigateur de le remplir automatiquement :
Text::make('password')->masked()->rules('required', 'string', 'min:12')Textarea
Saisie de texte sur plusieurs lignes. Même API que Text (seulement label()/rules()).
Textarea::make('body')->rules('nullable', 'string')RichEditor
Éditeur WYSIWYG (Tiptap côté frontend). Ses ~120 Ko de JS ne sont chargés que dans les panneaux qui utilisent ce champ. Le contenu est assaini côté serveur à chaque save() (via HtmlSanitizer), indépendamment de ce que le client envoie.
RichEditor::make('content')->rules('nullable', 'string')Toggle
Interrupteur booléen. Sans options propres au-delà de celles de la classe de base, default(false) est le default() générique de Field, avec false comme valeur par défaut du Toggle lui-même si vous n'en déclarez aucune autre.
Toggle::make('is_published')
->helperText('Published posts are visible on the public index.')
->default(false)Select
Liste déroulante à une seule option.
Select::make('category')
->options(['news' => 'News', 'update' => 'Update', 'announcement' => 'Announcement'])
->nullable()
->rules('nullable', 'string')| Méthode | Description |
|---|---|
options(array<string, string> $options) | Paires valeur => libellé. |
nullable(bool $nullable = true) | Permet de le laisser vide côté frontend. |
Les clés peuvent être des entiers, et la liste respecte l'ordre dans lequel vous déclarez les options.
CheckboxList
Sélection multiple sur le même format d'options que Select. Quand il y a plus d'une option, il affiche une case pour toutes les cocher ou décocher. Dans une Section repliée, l'en-tête indique combien de cases sont cochées.
CheckboxList::make('notify_channels')
->options(['email' => 'Email', 'push' => 'Push', 'sms' => 'SMS'])
->rules('nullable', 'array')Tags
Une liste libre de mots, enregistrée sous forme de tableau. L'utilisateur saisit une valeur et la confirme avec Entrée, virgule ou point-virgule, et chaque valeur devient une étiquette qu'on peut retirer. Les doublons sont ignorés.
Tags::make('keywords')
->itemRules('string', 'max:30')
->rules('nullable', 'array')itemRules() sont les règles de chaque élément, et rules() celles du tableau entier. On peut l'utiliser aussi bien dans un formulaire de Resource que dans le schema() d'une action.
Placeholder
Un champ en lecture seule qui affiche une valeur dans le formulaire, sans aucune saisie. La valeur provient des données du formulaire, vous la calculerez donc normalement dans mutateDataBeforeFill() :
Placeholder::make('summary')->label('Summary')Date et 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éthode | Description |
|---|---|
displayFormat(string $format) | Format d'affichage (syntaxe PHP date()). Y-m-d et H:i par défaut, respectivement. |
Date est affiché avec le DatePicker d'Arrel, qui en plus de la date admet un minimum, un maximum et des jours désactivés. Ces options sont des propriétés du composant, pas du champ PHP : vous les utilisez dans vos pages personnalisées.
File et Image
Image est un File avec un type() différent (image au lieu de file), ils partagent tout le reste de l'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éthode | Description |
|---|---|
disk(string $disk) | Disque de destination finale. local par défaut. |
directory(string $directory) | Sous-répertoire dans le disque. |
url(Closure $callback) | Résout un lien vers le fichier que l'enregistrement a déjà, pour que le formulaire d'édition affiche un « voir le fichier actuel ». Reçoit le modèle et retourne le lien, ou null s'il n'y en a pas. |
Comment fonctionne l'envoi : le frontend envoie le fichier dans un répertoire temporaire (arrel-temp, disque local) dès que l'utilisateur le sélectionne, avant d'enregistrer le formulaire. Lors du save(), Arrel déplace le fichier de arrel-temp vers {disk}/{directory}/{nom} et n'écrit qu'alors le chemin final en base de données (voir HasSchemaFields::moveUploadedFiles()). Les fichiers temporaires que personne n'enregistre sont nettoyés avec php artisan arrel:prune-uploads (voir Installation).
Repeater
Sous-schema répétable : une liste de groupes de champs de même forme.
Repeater::make('links')
->schema([
Text::make('label')->rules('required', 'string', 'max:255'),
Text::make('url')->rules('required', 'url'),
])
->rules('nullable', 'array')schema() prend un tableau de Field (rendu récursivement : un Repeater peut contenir un autre Repeater). Les règles de validation de chaque sous-champ s'appliquent avec la notation à points de Laravel pour les tableaux (links.*.label, links.*.url), générée automatiquement, et respectent le visibleWhen() propre à chaque ligne.
Relation
Une liste déroulante avec recherche sur un modèle lié. Elle a deux modes, selon que la relation porte une seule valeur ou plusieurs :
Valeur unique (belongsTo) : le champ porte le nom de la colonne de clé étrangère, pas celui de la méthode de relation, pour que le Model::create()/update() normal le traite comme n'importe quel autre attribut.
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')Multiple (belongsToMany, via ->multiple()) : le champ porte le nom de la méthode de la relation (tags, c'est-à-dire $model->tags()), pas d'une colonne, une table pivot n'a pas de clé étrangère pour le nommer. Utilisez ->relationship('nom') quand le nom du champ et celui de la méthode doivent différer.
Relation::make('tags')
->multiple()
->related(Tag::class)
->titleAttribute('name')
->rules('array')| Méthode | Description |
|---|---|
related(class-string<Model> $model) | Le modèle lié. Obligatoire. |
titleAttribute(string $attribute) | Attribut affiché dans chaque option de la liste. name par défaut. |
searchColumns(string ...$columns) | Colonnes sur lesquelles le combobox cherche. Par défaut, seulement titleAttribute(). |
query(Closure $callback) | Restreint les enregistrements qu'on peut choisir. S'applique aussi à la sauvegarde (voir plus bas). |
searchUsing(Closure $callback, ?Closure $refine = null) | Remplace la recherche par défaut de la liste. Voir « Recherche sur mesure ». |
dependsOn(string $field) | Les options dépendent de la valeur d'un autre champ du formulaire. Voir « Options dépendantes ». |
nullable(bool = true) | Pertinent uniquement en mode valeur unique. |
multiple(bool = true) | Passe le champ en sélection multiple (chips), sur une relation belongsToMany. |
relationship(string $name) | Nom de la méthode de relation, quand il diffère du nom du champ. Par défaut, le même nom que le champ. |
itemRules(string ...$rules) | Règles de validation pour chaque élément sélectionné en mode multiple. Par défaut, integer plus un exists:{table},{clé} dérivé de related(). |
La portée s'applique à la sauvegarde
query() ne filtre pas seulement la liste : il s'applique aussi à la sauvegarde. Un enregistrement que la portée masque est refusé avec une erreur de validation, même si quelqu'un envoie son identifiant à la main. Cela vaut pour les formulaires d'un Resource, d'un Relation manager et d'une action.
La closure reçoit trois arguments : le query builder, la valeur du champ de dependsOn() (ou null s'il n'y en a pas), et l'enregistrement parent quand le champ est dans un Relation manager ou une action. Elle doit modifier le builder qu'elle reçoit.
Relation::make('room_id')
->related(Room::class)
->query(function (Builder $query, mixed $dependsOn, ?Model $parent): void {
$query->where('school_id', $parent?->school_id);
})Options dépendantes
dependsOn() fait que les options d'un champ se rechargent quand un autre champ du formulaire change. La valeur de ce champ arrive comme second argument 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);
})Recherche sur mesure
Par défaut, la liste cherche avec un LIKE sur searchColumns(). searchUsing() le remplace pour les colonnes qui ne peuvent pas se comparer ainsi, comme une colonne chiffrée :
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 première closure resserre les candidats en SQL (jusqu'à 500). La seconde, refine, décide en PHP lesquels de ces candidats correspondent réellement. Sans refine, le résultat est ce que la première closure laisse dans le builder.
Comment une relation multiple est enregistrée : à la création/mise à jour de l'enregistrement, Arrel fait un sync() (remplacement complet, pas un ajout) sur la relation, avant l'exécution de afterCreate()/afterUpdate(), ces hooks voient donc toujours la relation déjà synchronisée. Comme sync() remplace tout l'ensemble des lignes pivot, toute colonne pivot supplémentaire est réinitialisée dans les lignes resynchronisées, modifier des données pivot est le travail d'un Relation manager, pas de ce champ. Une clé simplement absente du payload de la requête n'est jamais touchée, donc une mise à jour partielle ne peut pas supprimer par accident une relation existante.
Référence rapide
| Champ | type() | Options propres |
|---|---|---|
Text | text | masked() |
Textarea | textarea | Aucune |
RichEditor | rich_editor | Aucune |
Toggle | toggle | Aucune |
Select | select | options(), nullable() |
CheckboxList | checkbox_list | options() |
Tags | tags | itemRules() |
Placeholder | placeholder | Aucune |
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() |
Tous les champs partagent en outre helperText(), placeholder(), default(), visibleWhen() et visibleOn() de la classe de base.