Laravel Eloquent ORM (L13 — Attributes-first)
Agent Workflow (MANDATORY)
Before ANY implementation, spawn 3 agents in parallel, one Agent call each with a name:
- fuse-ai-pilot:explore-codebase - Inspect existing models, mixed property/attribute usage
- fuse-ai-pilot:research-expert - Verify Laravel 13 Eloquent + Attributes docs via Context7
- mcp__context7__query-docs - Query attribute patterns (#[Fillable], #[Casts], #[Scope])
After implementation, run fuse-ai-pilot:sniper for validation.
Overview
Laravel 13 promotes PHP 8.3 Attributes as the primary metadata mechanism on Eloquent models. Legacy properties ($fillable, $hidden, ...) remain supported for backward compatibility but should not be mixed with their attribute counterparts.
| Feature |
Attribute (L13 MAIN) |
Legacy property |
| Table name |
#[Table('users')] |
protected $table |
| Mass assignment |
#[Fillable([...])] |
protected $fillable |
| Hidden / Visible |
#[Hidden([...])] / #[Visible([...])] |
protected $hidden / $visible |
| Guarded |
#[Guarded([...])] / #[Unguarded] |
protected $guarded |
| Casts |
#[Casts([...])] |
casts() method |
| Appends |
#[Appends([...])] |
protected $appends |
| Touches |
#[Touches([...])] |
protected $touches |
| Connection |
#[Connection('mysql')] |
protected $connection |
Critical Rules
- Attributes are the source of truth - Use
#[Fillable], #[Casts], #[Hidden] on new code
- Never mix attribute + property for the same concern (
#[Fillable] AND $fillable)
- Eager load relationships - Prevent N+1 queries with
with()
- No
new Model() in boot() - Throws LogicException in L13 (booted lifecycle protected)
- Use factories in tests - Never hardcode test data
Architecture
app/Models/
├── User.php # #[Table], #[Fillable], #[Hidden], #[Casts]
├── Post.php # #[Connection], #[Appends], relationships
└── Concerns/
└── HasUuid.php # Reusable trait
→ See templates/ModelBasic.php.md
Reference Guide
Concepts
- Migration L12→L13: legacy-properties.md
- Modeling: models.md · casts.md · accessors-mutators.md · serialization.md · soft-deletes.md
- Relationships: relationships-basic.md · relationships-many-to-many.md · relationships-advanced.md · relationships-polymorphic.md
- Querying: eager-loading.md · scopes.md · aggregates.md · pagination.md · batch-operations.md · query-debugging.md
- Lifecycle / Output: events-observers.md · collections.md · resources.md · factories.md · transactions.md · performance.md
Templates
| Template |
When to Use |
| ModelBasic.php.md |
Attribute-based model |
| ModelRelationships.php.md |
All relationship types |
| ModelCasts.php.md |
#[Casts] and accessors |
| Observer.php.md |
Complete observer |
| Factory.php.md |
Factory with states |
| Resource.php.md |
API resource |
| EagerLoadingExamples.php.md |
N+1 prevention |
Quick Reference
Attribute-based Model (L13 MAIN)
use Illuminate\Database\Eloquent\Attributes\{Table, Fillable, Hidden, Casts};
use Illuminate\Database\Eloquent\Model;
#[Table('users')]
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
#[Casts(['email_verified_at' => 'datetime', 'is_admin' => 'boolean'])]
final class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
Scope (attribute syntax)
#[Scope]
protected function published(Builder $query): void
{
$query->whereNotNull('published_at');
}
// Usage: Post::published()->get();
Eager Loading
$posts = Post::with('author')->get(); // 2 queries, not N+1
→ Legacy $fillable / $hidden style — see legacy-properties.md
Best Practices
DO
- Declare metadata with PHP Attributes (
#[Table], #[Fillable], #[Casts], ...)
- Use
final on model classes when not extended
- Eager load with
with()
- Use factories in tests
- Cast dates, arrays, enums via
#[Casts]
DON'T
- Mix
#[Fillable] and $fillable on the same model (conflict — single source of truth)
- Instantiate models in
boot() / booted() — L13 throws LogicException
- Lazy-load relationships in loops (N+1)
- Use
#[Unguarded] in production
- Query inside accessors / mutators
- Put business logic in models (use Services/Actions)
1---2name: laravel-eloquent3description: Use when working with database models — Eloquent ORM, PHP attributes, relationships, queries, observers, or factories in Laravel 13.4---56<objective>7Covers Laravel 13 Eloquent ORM with PHP 8.3 Attributes as the primary8metadata mechanism (#[Table], #[Fillable], #[Hidden], #[Visible], #[Guarded],9#[Casts], #[Appends], #[Touches], #[Connection]) alongside legacy property10equivalents for backward compatibility. Includes all relationship types11(basic, many-to-many, advanced, polymorphic), eager loading, scopes,12accessors/mutators, events/observers, soft deletes, collections,13serialization, factories, API resources, transactions, pagination,14aggregates, batch operations, and query debugging/performance.15</objective>1617# Laravel Eloquent ORM (L13 — Attributes-first)1819## Agent Workflow (MANDATORY)2021Before ANY implementation, spawn 3 agents in parallel, one `Agent` call each with a `name`:22231. **fuse-ai-pilot:explore-codebase** - Inspect existing models, mixed property/attribute usage242. **fuse-ai-pilot:research-expert** - Verify Laravel 13 Eloquent + Attributes docs via Context7253. **mcp__context7__query-docs** - Query attribute patterns (#[Fillable], #[Casts], #[Scope])2627After implementation, run **fuse-ai-pilot:sniper** for validation.2829---3031## Overview3233Laravel 13 promotes **PHP 8.3 Attributes** as the primary metadata mechanism on Eloquent models. Legacy properties (`$fillable`, `$hidden`, ...) remain supported for backward compatibility but should not be mixed with their attribute counterparts.3435| Feature | Attribute (L13 MAIN) | Legacy property |36|---------|---------------------|-----------------|37| Table name | `#[Table('users')]` | `protected $table` |38| Mass assignment | `#[Fillable([...])]` | `protected $fillable` |39| Hidden / Visible | `#[Hidden([...])]` / `#[Visible([...])]` | `protected $hidden` / `$visible` |40| Guarded | `#[Guarded([...])]` / `#[Unguarded]` | `protected $guarded` |41| Casts | `#[Casts([...])]` | `casts()` method |42| Appends | `#[Appends([...])]` | `protected $appends` |43| Touches | `#[Touches([...])]` | `protected $touches` |44| Connection | `#[Connection('mysql')]` | `protected $connection` |4546---4748## Critical Rules49501. **Attributes are the source of truth** - Use `#[Fillable]`, `#[Casts]`, `#[Hidden]` on new code512. **Never mix attribute + property** for the same concern (`#[Fillable]` AND `$fillable`)523. **Eager load relationships** - Prevent N+1 queries with `with()`534. **No `new Model()` in `boot()`** - Throws `LogicException` in L13 (booted lifecycle protected)545. **Use factories** in tests - Never hardcode test data5556---5758## Architecture5960```61app/Models/62├── User.php # #[Table], #[Fillable], #[Hidden], #[Casts]63├── Post.php # #[Connection], #[Appends], relationships64└── Concerns/65 └── HasUuid.php # Reusable trait66```6768→ See [templates/ModelBasic.php.md](references/templates/ModelBasic.php.md)6970---7172## Reference Guide7374### Concepts7576- **Migration L12→L13:** [legacy-properties.md](references/legacy-properties.md)77- **Modeling:** [models.md](references/models.md) · [casts.md](references/casts.md) · [accessors-mutators.md](references/accessors-mutators.md) · [serialization.md](references/serialization.md) · [soft-deletes.md](references/soft-deletes.md)78- **Relationships:** [relationships-basic.md](references/relationships-basic.md) · [relationships-many-to-many.md](references/relationships-many-to-many.md) · [relationships-advanced.md](references/relationships-advanced.md) · [relationships-polymorphic.md](references/relationships-polymorphic.md)79- **Querying:** [eager-loading.md](references/eager-loading.md) · [scopes.md](references/scopes.md) · [aggregates.md](references/aggregates.md) · [pagination.md](references/pagination.md) · [batch-operations.md](references/batch-operations.md) · [query-debugging.md](references/query-debugging.md)80- **Lifecycle / Output:** [events-observers.md](references/events-observers.md) · [collections.md](references/collections.md) · [resources.md](references/resources.md) · [factories.md](references/factories.md) · [transactions.md](references/transactions.md) · [performance.md](references/performance.md)8182### Templates8384| Template | When to Use |85|----------|-------------|86| [ModelBasic.php.md](references/templates/ModelBasic.php.md) | Attribute-based model |87| [ModelRelationships.php.md](references/templates/ModelRelationships.php.md) | All relationship types |88| [ModelCasts.php.md](references/templates/ModelCasts.php.md) | #[Casts] and accessors |89| [Observer.php.md](references/templates/Observer.php.md) | Complete observer |90| [Factory.php.md](references/templates/Factory.php.md) | Factory with states |91| [Resource.php.md](references/templates/Resource.php.md) | API resource |92| [EagerLoadingExamples.php.md](references/templates/EagerLoadingExamples.php.md) | N+1 prevention |9394---9596## Quick Reference9798### Attribute-based Model (L13 MAIN)99100```php101use Illuminate\Database\Eloquent\Attributes\{Table, Fillable, Hidden, Casts};102use Illuminate\Database\Eloquent\Model;103104#[Table('users')]105#[Fillable(['name', 'email', 'password'])]106#[Hidden(['password', 'remember_token'])]107#[Casts(['email_verified_at' => 'datetime', 'is_admin' => 'boolean'])]108final class User extends Model109{110 public function posts(): HasMany111 {112 return $this->hasMany(Post::class);113 }114}115```116117### Scope (attribute syntax)118119```php120#[Scope]121protected function published(Builder $query): void122{123 $query->whereNotNull('published_at');124}125// Usage: Post::published()->get();126```127128### Eager Loading129130```php131$posts = Post::with('author')->get(); // 2 queries, not N+1132```133134→ Legacy `$fillable` / `$hidden` style — see [legacy-properties.md](references/legacy-properties.md)135136---137138## Best Practices139140### DO141- Declare metadata with **PHP Attributes** (`#[Table]`, `#[Fillable]`, `#[Casts]`, ...)142- Use `final` on model classes when not extended143- Eager load with `with()`144- Use factories in tests145- Cast dates, arrays, enums via `#[Casts]`146147### DON'T148- **Mix `#[Fillable]` and `$fillable`** on the same model (conflict — single source of truth)149- **Instantiate models in `boot()` / `booted()`** — L13 throws `LogicException`150- Lazy-load relationships in loops (N+1)151- Use `#[Unguarded]` in production152- Query inside accessors / mutators153- Put business logic in models (use Services/Actions)