Saltar al contenido

Laravel 13 incorpora una de las características más solicitadas por la comunidad: soporte nativo para PHP Attributes como forma alternativa de configurar modelos Eloquent, jobs, comandos Artisan, Form Requests, mailables, listeners y notificaciones.

Se trata de una API completamente opcional. Las propiedades de clase que usamos hasta ahora siguen funcionando exactamente igual. No hay ningún breaking change. Simplemente, ahora tienes una segunda forma —más declarativa y visual— de definir la configuración de tus clases.

Esta funcionalidad fue introducida por Taylor Otwell en el PR #58578 y mergeada en la rama principal el 13 de febrero de 2026.

Requisitos

  • Laravel 13 (previsto para Q1 2026)

  • PHP 8.3 o superior (requisito mínimo de Laravel 13)

¿Por qué PHP Attributes?

Hasta ahora, la configuración de un modelo Eloquent se definía mediante propiedades de clase:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $table = 'users';

    protected $primaryKey = 'user_id';

    protected $keyType = 'string';

    public $incrementing = false;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];
}

Este enfoque funciona, pero presenta algunas limitaciones. La configuración está dispersa entre las propiedades, los métodos y la lógica del modelo. Además, el framework ha ido adoptando PHP Attributes en distintos puntos durante los últimos años (como #[ObservedBy] o #[ScopedBy]), generando una inconsistencia donde algunas cosas se configuran con atributos y otras con propiedades.

Con la nueva API declarativa, el mismo modelo queda así:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('users', key: 'user_id', keyType: 'string', incrementing: false)]
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Model
{
    //
}

Toda la configuración queda agrupada encima de la clase, de un vistazo. El cuerpo del modelo queda libre para lo que realmente importa: relaciones, scopes, accessors y lógica de negocio.

Modelos Eloquent

El Attribute #[Table] es el más completo, ya que consolida varias propiedades relacionadas con la tabla en un solo lugar:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table(
    name: 'blog_posts',
    key: 'post_id',
    keyType: 'string',
    incrementing: false
)]
class Post extends Model
{
    //
}

Esto equivale a definir $table, $primaryKey, $keyType e $incrementing como propiedades individuales.

Timestamps

Si necesitas desactivar los timestamps o definir un formato de fecha personalizado:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Attributes\DateFormat;
use Illuminate\Database\Eloquent\Model;

#[Table('events')]
#[WithoutTimestamps]
#[DateFormat('Y-m-d')]
class Event extends Model
{
    //
}

Visibilidad y serialización

Los atributos #[Hidden], #[Visible] y #[Appends] controlan qué se incluye cuando el modelo se convierte a array o JSON:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('users')]
#[Hidden(['password', 'remember_token'])]
#[Appends(['full_name'])]
class User extends Model
{
    //
}

Asignación masiva

Para configurar $fillable o $guarded:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('products')]
#[Fillable(['name', 'price', 'description', 'category_id'])]
class Product extends Model
{
    //
}

Si prefieres trabajar sin protección de asignación masiva (como en muchos proyectos que usan Model::unguard() globalmente), puedes usar #[Unguarded]:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Unguarded;
use Illuminate\Database\Eloquent\Model;

#[Table('products')]
#[Unguarded]
class Product extends Model
{
    //
}

Relaciones touch

El atributo #[Touches] define qué relaciones deben actualizar su timestamp cuando el modelo se modifica:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Touches;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

#[Table('comments')]
#[Touches(['post'])]
class Comment extends Model
{
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

Conexión a base de datos

Si un modelo necesita conectarse a una base de datos diferente:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;

#[Table('audit_logs')]
#[Connection('audit')]
class AuditLog extends Model
{
    //
}

Lista completa de Attributes para modelos

Attribute

Equivale a

Descripción

#[Table]

$table, $primaryKey, $keyType, $incrementing

Configuración de tabla, clave primaria y tipo

#[WithoutIncrementing]

$incrementing = false

Desactiva auto-increment

#[Timestamps]

$timestamps = true

Activa timestamps (por defecto)

#[WithoutTimestamps]

$timestamps = false

Desactiva timestamps

#[DateFormat]

$dateFormat

Formato de serialización de fechas

#[Connection]

$connection

Conexión de base de datos

#[Fillable]

$fillable

Campos asignables masivamente

#[Guarded]

$guarded

Campos protegidos

#[Unguarded]

$guarded = []

Sin protección de asignación masiva

#[Hidden]

$hidden

Campos ocultos en serialización

#[Visible]

$visible

Campos visibles en serialización

#[Appends]

$appends

Accessors incluidos en serialización

#[Touches]

$touches

Relaciones con touch automático

Jobs y Colas

La configuración de jobs con atributos resulta especialmente útil porque agrupa todos los parámetros de la cola en un lugar visible:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

#[Connection('redis')]
#[Queue('podcasts')]
#[Tries(3)]
#[Timeout(120)]
#[DeleteWhenMissingModels]
class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private Podcast $podcast,
    ) {}

    public function handle(): void
    {
        // Lógica de procesamiento
    }
}

Comparado con el enfoque anterior donde estas propiedades se definían dentro de la clase:

<?php

// Enfoque con propiedades (sigue funcionando)
class ProcessPodcast implements ShouldQueue
{
    public $connection = 'redis';
    public $queue = 'podcasts';
    public $tries = 3;
    public $timeout = 120;
    public $deleteWhenMissingModels = true;

    // ...
}

Backoff y reintentos avanzados

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\UniqueFor;

#[Tries(5)]
#[Backoff(30, 60, 120)]
#[MaxExceptions(3)]
#[FailOnTimeout]
#[UniqueFor(3600)]
class SyncExternalData implements ShouldQueue
{
    // ...
}

Lista completa de Attributes para colas

Attribute

Equivale a

Descripción

#[Connection]

$connection

Conexión de cola

#[Queue]

$queue

Nombre de la cola

#[Tries]

$tries

Número máximo de intentos

#[Timeout]

$timeout

Tiempo máximo de ejecución (segundos)

#[Backoff]

$backoff

Segundos entre reintentos

#[MaxExceptions]

$maxExceptions

Máximo de excepciones antes de fallar

#[UniqueFor]

$uniqueFor

Tiempo de unicidad (segundos)

#[FailOnTimeout]

$failOnTimeout

Marcar como fallido en timeout

#[DeleteWhenMissingModels]

$deleteWhenMissingModels

Eliminar job si el modelo no existe

Estos atributos también funcionan en listeners, notifications, mailables y broadcast events que implementen ShouldQueue.

Comandos Artisan

Los comandos Artisan ahora pueden definir su firma y descripción como atributos:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('mail:send {user} {--queue}')]
#[Description('Send a marketing email to a user')]
class SendMailCommand extends Command
{
    public function handle(): void
    {
        $userId = $this->argument('user');
        $shouldQueue = $this->option('queue');

        // Lógica del comando
    }
}

Esto reemplaza las propiedades $signature y $description que definíamos dentro de la clase.

Form Requests

Los Form Requests pueden configurar la redirección en caso de fallo y el comportamiento de validación:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\Attributes\RedirectTo;
use Illuminate\Foundation\Http\Attributes\StopOnFirstFailure;
use Illuminate\Foundation\Http\FormRequest;

#[RedirectTo('/posts/create')]
#[StopOnFirstFailure]
class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string'],
            'category_id' => ['required', 'exists:categories,id'],
        ];
    }
}

Tests

En los tests, puedes definir qué seeder ejecutar con un atributo:

<?php

namespace Tests\Feature;

use Database\Seeders\OrderSeeder;
use Illuminate\Foundation\Testing\Attributes\Seeder;
use Tests\TestCase;

#[Seeder(OrderSeeder::class)]
class OrderTest extends TestCase
{
    public function test_orders_are_listed(): void
    {
        $response = $this->get('/orders');

        $response->assertOk();
    }
}

API Opcional: Propiedades y Attributes Coexisten

Es importante insistir en este punto: no necesitas migrar nada. Las propiedades de clase siguen siendo válidas y seguirán siéndolo. Los PHP Attributes son una alternativa, no un reemplazo.

Puedes incluso mezclar ambos enfoques en un mismo proyecto. Un modelo puede usar atributos mientras otro sigue con propiedades. La decisión es tuya y de tu equipo.

¿Cuándo usar cada enfoque?

PHP Attributes son ideales cuando:

  • Quieres ver toda la configuración de un vistazo, antes de entrar en la clase

  • Prefieres un estilo declarativo, similar a cómo trabajan frameworks como Symfony o Doctrine

  • Tienes modelos o jobs con mucha configuración que "ensucia" el cuerpo de la clase

Propiedades siguen siendo válidas cuando:

  • Ya tienes un proyecto con convenciones establecidas y no quieres mezclar estilos

  • Necesitas que la configuración sea dinámica o se determine en tiempo de ejecución

  • Trabajas con herencia y necesitas que los valores se propaguen a clases hijas (los atributos no se heredan por defecto)

Herencia: un detalle a tener en cuenta

Los PHP Attributes no se heredan de forma nativa. Si tienes un modelo base con #[Guarded(['id'])] y un modelo hijo con #[Guarded(['id', 'name'])], el hijo no acumulará ambos, solo usará los suyos propios. Esto es algo a considerar si trabajas con modelos base abstractos que definen configuración común.

Conclusión

Los PHP Attributes en Laravel 13 representan un paso natural hacia un framework más declarativo y consistente. No rompen nada, no obligan a migrar nada, pero ofrecen una forma más limpia y visual de configurar las clases que usamos a diario.

Para proyectos nuevos, merece la pena adoptar este enfoque desde el inicio. Para proyectos existentes, la migración puede hacerse gradualmente, clase a clase, sin presión.

Enlaces

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje