Skip to content

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.

php
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éthodePar défautDescription
model() (statique)AucunRetourne static::$model. Il faut déclarer protected static string $model.
slug() (statique)Str::plural(Str::kebab(...)) du nom de la classe sans le suffixe ResourceDéclarez protected static ?string $slug pour le remplacer. PostResource → posts.
label()Str::headline(...) du nom de la classe sans ResourcePostResource → « Post ». Remplacez-le pour le traduire.
pluralLabel()Str::plural($this->label())
php
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 ​

php
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.

php
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.

php
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 :

php
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 :

php
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.

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) :

php
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 par navigationGroup(), dans l'ordre où chaque groupe apparaît pour la première fois dans cette liste déjà triée, pas alphabétiquement. Un groupe null est 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 :

php
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.

php
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): ?Model doit retourner le modèle créé. Avec null, Arrel crée l'enregistrement avec Model::create().
  • handleRecordUpdate(Model $record, array $data): bool retourne true s'il a déjà fait la mise à jour. Avec false, Arrel fait $record->update($data).
  • handleRecordDeletion(Model $record): bool retourne true s'il a déjà fait la suppression. Avec false, Arrel fait $record->delete().
php
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) :

php
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() :

php
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 de TenantResolver::resolve() s'il y en a un lié dans le conteneur (voir Multi-tenancy) ; null sinon.

Pièces suivantes ​

Un Resource se construit en combinant :