Skip to content

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.

php
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étodoPor defectoDescripción
model() (estático)NingunoDevuelve static::$model. Hay que declarar protected static string $model.
slug() (estático)Str::plural(Str::kebab(...)) del nombre de la clase sin el sufijo ResourceDeclara protected static ?string $slug para sobrescribirlo. PostResource → posts.
label()Str::headline(...) del nombre de la clase sin ResourcePostResource → «Post». Sobrescríbelo para traducirlo.
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 query base ​

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

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

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' */ }

El comportamiento exacto:

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);
}

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:

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

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

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';
}

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 por navigationGroup(), en el orden en que cada grupo aparece por primera vez en esta lista ya ordenada, no alfabéticamente. Un grupo null es 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:

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() 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.

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);
}

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): ?Model debe devolver el modelo creado. Con null, Arrel crea el registro con Model::create().
  • handleRecordUpdate(Model $record, array $data): bool devuelve true si ya ha hecho la actualización. Con false, Arrel hace $record->update($data).
  • handleRecordDeletion(Model $record): bool devuelve true si ya ha hecho el borrado. Con false, Arrel hace $record->delete().
php
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):

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

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'),
            ]),
    ];
}

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 de TenantResolver::resolve() si hay uno enlazado en el contenedor (véase Multi-tenancy); null si no.

Siguientes piezas ​

Un Resource se construye combinando: