Laravel Eloquent ORM Reference Guide
A reference guide to Laravel's Eloquent ORM: models, relationships, eager loading, and query scopes.
Laravel Reference
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');
}
}
| Relationship | Meaning |
|---|---|
hasOne | This model owns exactly one related row (e.g. User → Profile). |
hasMany | This model owns many related rows (e.g. User → Posts). |
belongsTo | The inverse — this model holds the foreign key (e.g. Post → User). |
belongsToMany | Many-to-many via a pivot table (e.g. User ↔ Role). |
hasManyThrough | Access a distant relation through an intermediate model. |
morphMany / morphTo | Polymorphic 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.