Laravel Eloquent ORM Reference Guide

A reference guide to Laravel's Eloquent ORM: models, relationships, eager loading, and query scopes.

Defining a model

php artisan make:model Post -m   // -m also creates a migration
class Post extends Model
{
    protected $fillable = ['title', 'body', 'user_id'];

    protected $hidden = ['internal_notes'];

    protected $casts = [
        'published_at' => 'datetime',
        'is_featured' => 'boolean',
        'metadata' => 'array',
    ];
}

$fillable whitelists which attributes can be mass-assigned via create()/fill() — the counterpart $guarded blacklists instead. $hidden excludes attributes when the model is converted to an array or JSON. $casts converts attributes to native types (or Carbon instances for dates) automatically.

Basic CRUD

Post::create(['title' => 'Hello', 'body' => '...']);

Post::find(1);
Post::findOrFail(1);
Post::where('published', true)->first();
Post::all();

$post = Post::find(1);
$post->title = 'Updated title';
$post->save();

Post::where('id', 1)->update(['title' => 'Updated title']);

$post->delete();
Post::destroy([1, 2, 3]);

Relationships

class User extends Model
{
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
RelationshipMeaning
hasOneThis model owns exactly one related row (e.g. User → Profile).
hasManyThis model owns many related rows (e.g. User → Posts).
belongsToThe inverse — this model holds the foreign key (e.g. Post → User).
belongsToManyMany-to-many via a pivot table (e.g. User ↔ Role).
hasManyThroughAccess a distant relation through an intermediate model.
morphMany / morphToPolymorphic relationship — one relation type serving several model types.

Eager loading (avoiding N+1)

Accessing $post->user in a loop over many posts fires one extra query per post. Eager load the relationship up front instead:

// N+1: fires a query per post inside the loop
foreach (Post::all() as $post) {
    echo $post->user->name;
}

// One extra query total, not one per post
$posts = Post::with('user')->get();

$posts = Post::with(['user', 'comments'])->get();

// Load a relation on an already-fetched collection
$posts->load('comments');

N+1 problems are easy to introduce accidentally — a relation accessed inside a Blade @foreach, or inside an API resource's toArray(), looks identical to any other property access, so nothing warns you at the call site. Model::preventLazyLoading(), usually called in AppServiceProvider::boot() for the local environment, throws an exception the moment code lazy-loads a relationship instead of eager-loading it, which surfaces these during development rather than as a slow endpoint in production. For counting related rows without loading them at all, withCount('comments') adds a comments_count attribute using a single aggregate subquery.

Query scopes

class Post extends Model
{
    public function scopePublished(Builder $query): void
    {
        $query->where('published', true);
    }
}

Post::published()->latest()->get();

Accessors and mutators

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->first_name} {$this->last_name}",
            set: fn ($value) => ['first_name' => explode(' ', $value)[0]],
        );
    }
}

$user->full_name; // uses the get callback

Soft deletes

Adding the SoftDeletes trait means delete() sets a deleted_at timestamp instead of removing the row, and every query automatically excludes soft-deleted records without you having to remember to filter them out. The model needs a nullable deleted_at column, typically added with $table->softDeletes() in a migration.

class Post extends Model
{
    use SoftDeletes;
}

$post->delete();          // sets deleted_at, row stays in the table
Post::find(1);             // null — soft-deleted rows are excluded by default

Post::withTrashed()->find(1);   // include soft-deleted rows
Post::onlyTrashed()->get();     // only soft-deleted rows
$post->restore();               // clear deleted_at
$post->forceDelete();           // actually remove the row

Soft deletes are worth using whenever "deleted" data still has audit, undo, or reporting value — but every relationship touching that model needs to account for it, since a soft-deleted parent's children don't get soft-deleted automatically, and unique constraints in the database won't know to ignore trashed rows either.

Factories and seeders

Factories describe how to generate a fake but realistic instance of a model, which is invaluable for tests and for populating a local database with representative data instead of hand-writing rows.

php artisan make:factory PostFactory
php artisan make:seeder PostSeeder
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'body' => fake()->paragraphs(3, true),
            'user_id' => User::factory(),
        ];
    }
}

Post::factory()->count(20)->create();
Post::factory()->create(['title' => 'Fixed title']);

// A named state for a common variation
Post::factory()->count(5)->published()->create();

Seeders drive factories (or insert fixed reference data) and are run with php artisan db:seed, or automatically as part of php artisan migrate:fresh --seed when rebuilding a local database from scratch.