Resources
Un Resource est une classe PHP qui décrit un modèle Eloquent : quels champs a son formulaire, quelles colonnes a son tableau, quels filtres, quelles actions et quelles relations sont gérés depuis son panneau. Arrel transforme cette description en un endpoint /schema que le frontend affiche dynamiquement ; vous ne générez aucun fichier Vue.
final class PostResource extends Resource
{
protected static string $model = Post::class;
public function fields(): array { /* ... */ }
public function columns(): array { /* ... */ }
}fields() et columns() sont abstraits : tout Resource doit les implémenter. actions(), filters() et relations() ont une implémentation par défaut qui retourne un tableau vide, et vous ne les ajoutez que lorsque vous en avez besoin.
Identité
| Méthode | Par défaut | Description |
|---|---|---|
model() (statique) | Aucun | Retourne static::$model. Il faut déclarer protected static string $model. |
slug() (statique) | Str::plural(Str::kebab(...)) du nom de la classe sans le suffixe Resource | Déclarez protected static ?string $slug pour le remplacer. PostResource → posts. |
label() | Str::headline(...) du nom de la classe sans Resource | PostResource → « Post ». Remplacez-le pour le traduire. |
pluralLabel() | Str::plural($this->label()) |
final class PostResource extends Resource
{
protected static string $model = Post::class;
protected static ?string $slug = 'articles';
public function label(): string
{
return __('posts.model_label');
}
}La requête de base
public function getEloquentQuery(): Builder
{
$model = static::model();
return $model::query();
}Remplacez-la pour appliquer n'importe quel scope global à la liste et à la recherche d'enregistrements (visibilité par rôle, soft deletes, relations préchargées avec with()...). C'est une méthode normale de la classe : vous pouvez appeler $this->user() ou $this->tenant() (voir plus bas) depuis ici.
public function defaultSort(): ?string
{
return '-published_at'; // le préfixe `-` inverse l'ordre
}Autorisation
Arrel n'a pas de DSL d'autorisation propre : les cinq méthodes de vérification délèguent directement à la Policy Laravel du modèle, quand il y en a une d'enregistrée.
public function canViewAny(): bool
{
return $this->authorize('viewAny', static::model());
}
public function canView(Model $record): bool { /* 'view' */ }
public function canCreate(): bool { /* 'create' */ }
public function canEdit(Model $record): bool { /* 'update' */ }
public function canDelete(Model $record): bool { /* 'delete' */ }Le comportement exact :
protected function authorize(string $ability, Model|string $arg): bool
{
if (Gate::getPolicyFor($arg) === null) {
return true;
}
return Gate::forUser($this->user())->allows($ability, $arg);
}Sans Policy enregistrée pour le modèle, tout est permis à n'importe quel utilisateur authentifié. Quand il y en a une, Arrel lui délègue : rien de nouveau à apprendre, les mêmes méthodes viewAny/view/create/update/delete que vous utilisez déjà dans le reste de l'application. Notez que canEdit() vérifie l'habilité update (pas edit), le nom que Laravel utilise par convention dans ses Policies.
Remplacez n'importe laquelle des cinq méthodes si vous avez besoin d'une logique différente de votre Policy pour le contexte du panneau :
public function canDelete(Model $record): bool
{
return $this->user()->can('posts.delete')
&& ! $record->isProtected();
}canViewAny() ferme le Resource entier. S'il retourne faux, tous les endpoints du Resource répondent 403 (liste, schéma, création, édition, suppression, actions, relations et options des champs), pas seulement la liste. C'est pourquoi un Resource qui doit être restreint n'a besoin que d'une Policy avec viewAny, ou d'un canViewAny() propre. Voir Sécurité et autorisation.
Navigation
Quatre méthodes contrôlent la façon dont le Resource apparaît en dehors de son propre formulaire/tableau. Elles retournent toutes null par défaut (pas d'icône, pas de groupe, pas d'ordre explicite, pas de titre par enregistrement) :
public function icon(): ?string
{
return 'document-text';
}
public function navigationGroup(): ?string
{
return 'school';
}
public function navigationSort(): ?int
{
return 10;
}
public function recordTitleAttribute(): ?string
{
return 'title';
}Ce sont des méthodes d'instance normales, comme label()/pluralLabel(), pas des propriétés statiques : l'icône ou la position dans la navigation relève de la présentation, pas de l'identité.
icon()doit être l'un des noms du sous-ensemble choisi de Heroicons (voir Actions → Icônes) ; un nom qui n'y figure pas n'affiche rien.navigationSort()trie les Resources par ordre croissant ; un Resource qui n'en déclare aucun est placé après tous ceux qui en ont un, en conservant son ordre d'enregistrement d'origine par rapport aux autres sans ordre. Ils sont ensuite regroupés parnavigationGroup(), dans l'ordre où chaque groupe apparaît pour la première fois dans cette liste déjà triée, pas alphabétiquement. Un groupenullest son propre casier (les Resources sans groupe ne sont ni mélangés ni écartés).recordTitleAttribute()nomme un champ ou une colonne qui s'affiche dans l'en-tête de la page d'édition/vue d'un enregistrement précis (p. ex. « Edit Post: Guide d'installation ») quand l'enregistrement chargé en a une valeur. S'il n'est pas défini, l'en-tête reste sur le « Edit {label} » générique.
Lifecycle hooks
Les hooks encadrent les requêtes de création, de mise à jour et de suppression d'un enregistrement :
public function mutateDataBeforeCreate(array $data): array;
public function handleRecordCreation(array $data): ?Model;
public function afterCreate(Model $record): void;
public function mutateDataBeforeUpdate(array $data, Model $record): array;
public function handleRecordUpdate(Model $record, array $data): bool;
public function afterUpdate(Model $record): void;
public function handleRecordDeletion(Model $record): bool;
public function mutateDataBeforeFill(array $data, Model $record): array;mutateDataBeforeCreate()/mutateDataBeforeUpdate() s'exécutent après la validation et avant l'écriture de l'enregistrement, et retournent le tableau qui est effectivement persisté. afterCreate()/afterUpdate() s'exécutent une fois enregistré (et, pour afterCreate(), rechargé), de sorte que $record a déjà sa clé primaire.
public function mutateDataBeforeCreate(array $data): array
{
if (isset($data['category'])) {
$data['category'] = strtolower($data['category']);
}
return $data;
}
public function afterCreate(Model $record): void
{
if (! $record instanceof Post || $record->tags()->exists()) {
return;
}
$tag = Tag::firstOrCreate(['name' => 'general']);
$record->tags()->attach($tag->id);
}Remplacer la façon dont l'enregistrement est écrit
handleRecordCreation(), handleRecordUpdate() et handleRecordDeletion() remplacent l'écriture par défaut (Model::create(), update() et delete()) quand le modèle ne peut pas être enregistré ainsi : un service de domaine, une transaction propre, un modèle qui se supprime avec une règle métier.
handleRecordCreation(array $data): ?Modeldoit retourner le modèle créé. Avecnull, Arrel crée l'enregistrement avecModel::create().handleRecordUpdate(Model $record, array $data): boolretournetrues'il a déjà fait la mise à jour. Avecfalse, Arrel fait$record->update($data).handleRecordDeletion(Model $record): boolretournetrues'il a déjà fait la suppression. Avecfalse, Arrel fait$record->delete().
public function handleRecordCreation(array $data): ?Model
{
return app(CreatePost::class)->handle($data);
}Une RuntimeException levée pendant la suppression reçoit un 422 et son message.
Remplir le formulaire d'édition
mutateDataBeforeFill() reçoit les données envoyées au formulaire d'édition d'un enregistrement et retourne celles qui y sont réellement affichées. Il sert à calculer des valeurs qui ne sont pas une colonne (par exemple, le texte d'un champ Placeholder) :
public function mutateDataBeforeFill(array $data, Model $record): array
{
$data['summary'] = "{$record->comments()->count()} comments";
return $data;
}Tous les hooks ont une implémentation par défaut qui ne fait rien ; vous ne les remplacez que lorsque vous en avez besoin.
Page de détail
Quand un Resource a une ViewAction, l'enregistrement s'affiche dans une page en lecture seule. Par défaut, cette page réutilise les columns() du Resource. Pour l'organiser en sections, déclarez infolist() :
public function infolist(): array
{
return [
InfolistSection::make('Post')
->columns(2)
->schema([
Column::make('title'),
Column::make('status')->as('badge'),
]),
InfolistSection::make('Dates')
->schema([
Column::make('published_at')->as('date')->displayFormat('d/m/Y'),
]),
];
}Chaque entrée est une Column, avec les mêmes as(), value() et color(). InfolistSection::make($label) accepte columns(int) pour la grille. Avec un infolist() vide (la valeur par défaut), la vue utilise columns().
Helpers disponibles
Dans n'importe quelle méthode du Resource, vous avez accès à :
$this->user(): ?Authenticatable: l'utilisateur authentifié actuel.$this->tenant(): mixed: le résultat deTenantResolver::resolve()s'il y en a un lié dans le conteneur (voir Multi-tenancy) ;nullsinon.
Pièces suivantes
Un Resource se construit en combinant :
- Champs : ce que contient le formulaire.
- Tableaux et colonnes : ce qui s'affiche dans la liste.
- Filtres : comment la liste se filtre.
- Actions : ce qu'on peut faire de chaque enregistrement.
- Relation managers : comment les relations se gèrent depuis le même panneau.