Resources
Un Resource es una clase PHP que describe un modelo Eloquent: qué campos tiene su formulario, qué columnas tiene su tabla, qué filtros, qué acciones y qué relaciones se gestionan desde su panel. Arrel convierte esta descripción en un endpoint /schema que el frontend renderiza dinámicamente; no generas ningún archivo Vue.
final class PostResource extends Resource
{
protected static string $model = Post::class;
public function fields(): array { /* ... */ }
public function columns(): array { /* ... */ }
}fields() y columns() son abstractos: todo Resource debe implementarlos. actions(), filters() y relations() tienen una implementación por defecto que devuelve un array vacío, y los añades solo cuando los necesitas.
Identidad
| Método | Por defecto | Descripción |
|---|---|---|
model() (estático) | Ninguno | Devuelve static::$model. Hay que declarar protected static string $model. |
slug() (estático) | Str::plural(Str::kebab(...)) del nombre de la clase sin el sufijo Resource | Declara protected static ?string $slug para sobrescribirlo. PostResource → posts. |
label() | Str::headline(...) del nombre de la clase sin Resource | PostResource → «Post». Sobrescríbelo para traducirlo. |
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();
}Sobrescríbelo para aplicar cualquier scope global al listado y a la búsqueda de registros (visibilidad por rol, soft deletes, relaciones precargadas con with()...). Es un método normal de la clase: puedes llamar a $this->user() o $this->tenant() (véase más abajo) desde aquí.
public function defaultSort(): ?string
{
return '-published_at'; // el prefijo `-` invierte el orden
}Autorización
Arrel no tiene un DSL de autorización propio: los cinco métodos de comprobación delegan directamente en la Policy de Laravel del modelo, cuando hay 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 comportamiento exacto:
protected function authorize(string $ability, Model|string $arg): bool
{
if (Gate::getPolicyFor($arg) === null) {
return true;
}
return Gate::forUser($this->user())->allows($ability, $arg);
}Sin Policy registrada para el modelo, todo queda permitido para cualquier usuario autenticado. Al tener una, Arrel delega en ella: no hay que aprender nada nuevo, los mismos métodos viewAny/view/create/update/delete que ya usas en el resto de la aplicación. Ten en cuenta que canEdit() comprueba la habilidad update (no edit), el nombre que Laravel usa por convención en sus Policies.
Sobrescribe cualquiera de los cinco métodos si necesitas una lógica distinta de tu Policy para el contexto del panel:
public function canDelete(Model $record): bool
{
return $this->user()->can('posts.delete')
&& ! $record->isProtected();
}canViewAny() cierra el Resource entero. Si devuelve falso, todos los endpoints del Resource responden 403 (listado, esquema, creación, edición, borrado, acciones, relaciones y opciones de los campos), no solo el listado. Por eso, un Resource que debe ser restringido solo necesita una Policy con viewAny, o un canViewAny() propio. Véase Seguridad y autorización.
Navegación
Cuatro métodos controlan cómo aparece el Resource fuera de su propio formulario/tabla. Todos devuelven null por defecto (sin icono, sin grupo, sin orden explícito, sin título por registro):
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';
}Son métodos de instancia normales, como label()/pluralLabel(), no propiedades estáticas: el icono o la posición en la navegación es presentación, no identidad.
icon()debe ser uno de los nombres del subconjunto curado de Heroicons (véase Acciones → Iconos); un nombre que no está no renderiza nada.navigationSort()ordena los Resources de manera ascendente; uno que no declara ninguno se ordena después de todos los que sí, manteniendo su orden de registro original respecto a los demás sin orden. Después se agrupan pornavigationGroup(), en el orden en que cada grupo aparece por primera vez en esta lista ya ordenada, no alfabéticamente. Un gruponulles su propio cajón (los Resources sin grupo ni se mezclan ni se descartan).recordTitleAttribute()nombra un campo o columna que se muestra en la cabecera de la página de edición/vista de un registro concreto (p. ej. «Edit Post: Guía de instalación») cuando el registro cargado tiene un valor para él. Si no se define, la cabecera se queda con el «Edit {label}» genérico.
Lifecycle hooks
Los hooks envuelven las peticiones de crear, actualizar y borrar un registro:
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() se ejecutan después de la validación y antes de escribir el registro, y devuelven el array que efectivamente se persiste. afterCreate()/afterUpdate() se ejecutan una vez guardado (y, en el caso de afterCreate(), recargado), así que $record ya tiene su clave primaria.
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);
}Sustituir cómo se escribe el registro
handleRecordCreation(), handleRecordUpdate() y handleRecordDeletion() sustituyen la escritura por defecto (Model::create(), update() y delete()) cuando el modelo no se puede guardar así: un servicio de dominio, una transacción propia, un modelo que se borra con una regla de negocio.
handleRecordCreation(array $data): ?Modeldebe devolver el modelo creado. Connull, Arrel crea el registro conModel::create().handleRecordUpdate(Model $record, array $data): booldevuelvetruesi ya ha hecho la actualización. Confalse, Arrel hace$record->update($data).handleRecordDeletion(Model $record): booldevuelvetruesi ya ha hecho el borrado. Confalse, Arrel hace$record->delete().
public function handleRecordCreation(array $data): ?Model
{
return app(CreatePost::class)->handle($data);
}Una RuntimeException lanzada durante el borrado se responde con un 422 y su mensaje.
Rellenar el formulario de edición
mutateDataBeforeFill() recibe los datos que se envían al formulario de edición de un registro y devuelve los que realmente se muestran en él. Sirve para calcular valores que no son una columna (por ejemplo, el texto de un campo Placeholder):
public function mutateDataBeforeFill(array $data, Model $record): array
{
$data['summary'] = "{$record->comments()->count()} comments";
return $data;
}Todos los hooks tienen una implementación por defecto que no hace nada; solo los sobrescribes cuando los necesitas.
Página de detalle
Cuando un Resource tiene una ViewAction, el registro se muestra en una página de solo lectura. Por defecto, esta página reutiliza las columns() del Resource. Para organizarla en secciones, 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 es una Column, con el mismo as(), value() y color(). InfolistSection::make($label) acepta columns(int) para la cuadrícula. Con infolist() vacío (el valor por defecto), la vista usa columns().
Helpers disponibles
Dentro de cualquier método del Resource tienes acceso a:
$this->user(): ?Authenticatable: el usuario autenticado actual.$this->tenant(): mixed: el resultado deTenantResolver::resolve()si hay uno enlazado en el contenedor (véase Multi-tenancy);nullsi no.
Siguientes piezas
Un Resource se construye combinando:
- Campos: qué contiene el formulario.
- Tablas y columnas: qué se muestra en el listado.
- Filtros: cómo se filtra el listado.
- Acciones: qué se puede hacer con cada registro.
- Relation managers: cómo se gestionan las relaciones desde el mismo panel.