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 |
|---|---|---|
|
| Configuración de tabla, clave primaria y tipo |
|
| Desactiva auto-increment |
|
| Activa timestamps (por defecto) |
|
| Desactiva timestamps |
|
| Formato de serialización de fechas |
|
| Conexión de base de datos |
|
| Campos asignables masivamente |
|
| Campos protegidos |
|
| Sin protección de asignación masiva |
|
| Campos ocultos en serialización |
|
| Campos visibles en serialización |
|
| Accessors incluidos en serialización |
|
| 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 |
|---|---|---|
|
| Conexión de cola |
|
| Nombre de la cola |
|
| Número máximo de intentos |
|
| Tiempo máximo de ejecución (segundos) |
|
| Segundos entre reintentos |
|
| Máximo de excepciones antes de fallar |
|
| Tiempo de unicidad (segundos) |
|
| Marcar como fallido en timeout |
|
| 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
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.
star Incluido en cualquier suscripción