5 min read

Laravel 13 PHP Attributes: Cleaner Eloquent Models Without Breaking Anything

Laravel 13 lets you declare Eloquent config like fillable, hidden, and table with native PHP attributes. Here is how the new syntax works in practice.

Featured image for "Laravel 13 PHP Attributes: Cleaner Eloquent Models Without Breaking Anything"

For as long as I have written Eloquent models, the top of every class has been a wall of protected properties. $table, $primaryKey, $keyType, $incrementing, $fillable, $guarded, $hidden, $casts. None of it is business logic, but all of it sits between the class declaration and the methods that actually do something. Laravel 13, which shipped on March 17, 2026, gives us a different option. You can now declare a lot of that configuration as native PHP attributes above the class, and the protected properties you have always used keep working exactly as before.

This is the kind of change I appreciate. It is purely additive, it leans on a language feature PHP has had since 8.0, and it does not ask you to rewrite a single existing model. Let me walk through what is actually new, show the before and after, and talk about when it is worth reaching for.

The Old Way, Which Still Works

Here is a model the way most of us have written them for years.

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    protected $table = 'flights';
    protected $primaryKey = 'flight_id';
    public $incrementing = false;
    protected $keyType = 'string';

    protected $fillable = ['name', 'airline', 'departs_at'];
    protected $hidden = ['internal_notes'];

    public function bookings()
    {
        return $this->hasMany(Booking::class);
    }
}

There is nothing wrong with that. It is explicit and it reads fine. The only real complaint is that the configuration and the relationships live in the same visual space, and on a large model the config can push the interesting code well below the fold.

The New Way With Attributes

Laravel 13 introduces a set of Eloquent attributes that move that configuration above the class. The same model looks like this.

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

#[Table('flights', key: 'flight_id', keyType: 'string', incrementing: false)]
#[Fillable(['name', 'airline', 'departs_at'])]
#[Hidden(['internal_notes'])]
class Flight extends Model
{
    public function bookings()
    {
        return $this->hasMany(Booking::class);
    }
}

The class body is now just relationships and behavior. The #[Table] attribute is the workhorse here. It bundles several properties that used to be separate: the table name, the primary key column, the key type, whether the key auto-increments, and it can also control timestamps and the date format. One attribute replaces four or five property declarations.

The #[Fillable] and #[Hidden] attributes do what their names suggest, taking over from $fillable and $hidden. There is a matching #[Guarded] for the inverse mass-assignment approach. Laravel Daily counted roughly three dozen new attributes across the framework once you include controllers, jobs, commands, listeners, mailables, and notifications, so this is not a one-off for models. It is a consistent pattern the framework now supports in more than fifteen places.

Why Attributes Instead of Properties

The argument for this is mostly about reading code, not writing it. Configuration that describes the class as a whole arguably belongs to the class declaration, not buried in its body. Attributes make that metadata visible at a glance, right where you look first. When you open a model, you immediately see which table it maps to and what is mass-assignable, without scanning for property names.

There is also a static analysis angle. Attributes are first-class language constructs that tools can read through reflection without booting your application. That opens the door to better IDE support, custom tooling, and framework features that introspect your models more cheaply than parsing property values at runtime.

That said, I want to be honest about the tradeoffs. Attributes are slightly more verbose for simple cases. A bare protected $table = 'flights'; is shorter than importing and applying #[Table('flights')]. And mixing styles within a single codebase can look inconsistent if a team does not agree on a convention. Which brings me to the most important practical point.

It Is Fully Backward Compatible

Nothing here is a breaking change. Laravel 13 launched with a zero breaking changes promise and a roughly ten minute upgrade path from Laravel 12, and the attribute system fits that philosophy. Every existing model with protected properties keeps working untouched. You can adopt attributes on new models only, leave old ones alone, and the framework resolves both approaches happily.

You can even mix the two on the same class. The framework will read a #[Table('users')] attribute while still honoring a protected $fillable property below it. I would not recommend mixing them on the same model as a habit, because consistency within a file matters for readability, but it is reassuring to know a half-migrated class will not break.

#[Table('users')]
class User extends Model
{
    // Still works alongside the attribute above
    protected $fillable = ['name', 'email', 'password'];
}

How I Would Roll This Out

My advice is to treat this as a style decision and make it deliberately, not reactively. If you are starting a new Laravel 13 project, adopting attributes from day one gives you clean, scannable models with no migration cost. If you have a large existing application, there is no reason to do a sweeping refactor. The old syntax is not deprecated, and a giant pull request that touches every model just to move properties into attributes is risk without much reward.

A reasonable middle path is to adopt attributes on new models and on any model you are already refactoring for other reasons. Write the convention down in your contributing guide so the team is aligned, and let the codebase converge over time. PHP 8.3 is the minimum for Laravel 13, so the language support is guaranteed to be there.

For me, the appeal is simple. Models are where a Laravel app’s data shape lives, and anything that makes them easier to read at a glance is worth considering. Attributes do that without forcing my hand, and that combination of a real improvement with zero obligation is exactly the sort of change I want from a major framework release.

Sources