Resources
Un Resource és una classe PHP que descriu un model Eloquent: quins camps té el seu formulari, quines columnes té la seva taula, quins filtres, quines accions i quines relacions es gestionen des del seu panell. Arrel converteix aquesta descripció en un endpoint /schema que el frontend renderitza dinàmicament; no generes cap fitxer Vue.
final class PostResource extends Resource
{
protected static string $model = Post::class;
public function fields(): array { /* ... */ }
public function columns(): array { /* ... */ }
}fields() i columns() són abstractes: tot Resource els ha d'implementar. actions(), filters() i relations() tenen una implementació per defecte que retorna un array buit, els afegeixes només quan els necessites.
Identitat
| Mètode | Per defecte | Descripció |
|---|---|---|
model() (estàtic) | Cap | Retorna static::$model. Cal declarar protected static string $model. |
slug() (estàtic) | Str::plural(Str::kebab(...)) del nom de la classe sense el sufix Resource | Declara protected static ?string $slug per sobreescriure'l. PostResource → posts. |
label() | Str::headline(...) del nom de la classe sense Resource | PostResource → «Post». Sobreescriu-lo per traduir-lo. |
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 query base
public function getEloquentQuery(): Builder
{
$model = static::model();
return $model::query();
}Sobreescriu-lo per aplicar qualsevol scope global al llistat i a la cerca de registres (visibilitat per rol, soft deletes, relacions precarregades amb with()...). És un mètode normal de la classe: pots cridar $this->user() o $this->tenant() (vegeu més avall) des d'aquí.
public function defaultSort(): ?string
{
return '-published_at'; // el prefix `-` inverteix l'ordre
}Autorització
Arrel no té un DSL d'autorització propi: els cinc mètodes de comprovació deleguen directament a la Policy de Laravel del model, quan n'hi ha una registrada.
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' */ }El comportament exacte:
protected function authorize(string $ability, Model|string $arg): bool
{
if (Gate::getPolicyFor($arg) === null) {
return true;
}
return Gate::forUser($this->user())->allows($ability, $arg);
}Sense Policy registrada per al model, tot queda permès per a qualsevol usuari autenticat. En tenir-ne una, Arrel hi delega: no cal aprendre res nou, els mateixos mètodes viewAny/view/create/update/delete que ja fas servir a la resta de l'aplicació. Nota que canEdit() comprova l'habilitat update (no edit), el nom que Laravel fa servir per convenció a les seves Policies.
Sobreescriu qualsevol dels cinc mètodes si necessites lògica diferent de la teva Policy per al context del panell:
public function canDelete(Model $record): bool
{
return $this->user()->can('posts.delete')
&& ! $record->isProtected();
}canViewAny() tanca el Resource sencer. Si retorna fals, tots els endpoints del Resource responen 403 (llistat, esquema, creació, edició, esborrat, accions, relacions i opcions dels camps), no només el llistat. Per això, un Resource que ha de ser restringit només necessita una Policy amb viewAny, o un canViewAny() propi. Vegeu Seguretat i autorització.
Navegació
Quatre mètodes controlen com apareix el Resource fora del seu propi formulari/taula. Tots retornen null per defecte (sense icona, sense grup, sense ordre explícit, sense títol per registre):
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';
}Són mètodes d'instància normals, com label()/pluralLabel(), no propietats estàtiques: la icona o la posició a la navegació és presentació, no identitat.
icon()ha de ser un dels noms del subconjunt curat de Heroicons (vegeu Accions → Icones); un nom que no hi és no renderitza res.navigationSort()ordena els Resources de manera ascendent; un que no en declara cap s'ordena després de tots els que sí, mantenint el seu ordre de registre original respecte als altres sense ordre. Després s'agrupen pernavigationGroup(), en l'ordre en què cada grup apareix per primer cop en aquesta llista ja ordenada, no alfabèticament. Un grupnullés el seu propi calaix (els Resources sense grup no es barregen ni es descarten).recordTitleAttribute()anomena un camp o columna que es mostra a la capçalera de la pàgina d'edició/vista d'un registre concret (p. ex. «Edit Post: Guia d'instal·lació») quan el registre carregat en té un valor. Sense definir-lo, la capçalera es queda amb el «Edit {label}» genèric.
Lifecycle hooks
Els hooks envolten les peticions de crear, actualitzar i esborrar un registre:
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'executen després de la validació i abans d'escriure el registre, i retornen l'array que efectivament es persisteix. afterCreate()/afterUpdate() s'executen un cop desat (i, en el cas d'afterCreate(), recarregat), així que $record ja té la seva clau primària.
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);
}Substituir com s'escriu el registre
handleRecordCreation(), handleRecordUpdate() i handleRecordDeletion() substitueixen l'escriptura per defecte (Model::create(), update() i delete()) quan el model no es pot desar així: un servei de domini, una transacció pròpia, un model que s'esborra amb una regla de negoci.
handleRecordCreation(array $data): ?Modelha de retornar el model creat. Ambnull, Arrel crea el registre ambModel::create().handleRecordUpdate(Model $record, array $data): boolretornatruesi ja ha fet l'actualització. Ambfalse, Arrel fa$record->update($data).handleRecordDeletion(Model $record): boolretornatruesi ja ha fet l'esborrat. Ambfalse, Arrel fa$record->delete().
public function handleRecordCreation(array $data): ?Model
{
return app(CreatePost::class)->handle($data);
}Una RuntimeException llançada durant l'esborrat es respon amb un 422 i el seu missatge.
Omplir el formulari d'edició
mutateDataBeforeFill() rep les dades que s'envien al formulari d'edició d'un registre i retorna les que realment s'hi mostren. Serveix per calcular valors que no són una columna (per exemple, el text d'un camp Placeholder):
public function mutateDataBeforeFill(array $data, Model $record): array
{
$data['summary'] = "{$record->comments()->count()} comments";
return $data;
}Tots els hooks tenen una implementació per defecte que no fa res; només els sobreescrius quan els necessites.
Pàgina de detall
Quan un Resource té una ViewAction, el registre es mostra en una pàgina de només lectura. Per defecte, aquesta pàgina reutilitza les columns() del Resource. Per organitzar-la en seccions, declara 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'),
]),
];
}Cada entrada és una Column, amb el mateix as(), value() i color(). InfolistSection::make($label) accepta columns(int) per a la graella. Amb infolist() buit (el valor per defecte), la vista fa servir columns().
Helpers disponibles
Dins de qualsevol mètode del Resource tens accés a:
$this->user(): ?Authenticatable: l'usuari autenticat actual.$this->tenant(): mixed: el resultat deTenantResolver::resolve()si n'hi ha un enllaçat al contenidor (vegeu Multi-tenancy);nullsi no.
Següents peces
Un Resource es construeix combinant:
- Camps: què conté el formulari.
- Taules i columnes: què es mostra al llistat.
- Filtres: com es filtra el llistat.
- Accions: què es pot fer amb cada registre.
- Relation managers: com es gestionen les relacions des del mateix panell.