1. Introduction to Eloquent ORM
Laravel's Eloquent ORM is an ActiveRecord implementation that lets every database table have a corresponding Model class. Models handle querying, inserting, updating, and deleting records — and they define relationships to other models.
In Laravel 12/13, Eloquent relationships work the same as before but now benefit from:
- Typed return types on all relationship methods (enforced by IDE tooling)
php artisan make:model now generates typed relationship stubs
- Improved eager loading memory usage
- Full support for PHP 8.3+ features like typed constants and readonly properties
💡 Convention First Eloquent assumes foreign keys follow the pattern {model_name}_id and primary key is id. You can override both in every relationship method if your schema differs.
Relationship Overview
One to One
User → Profile. One parent record maps to exactly one child record.
One to Many
Post → Comments. One parent has many child records.
Many to Many
User ↔ Roles. Both sides can have many of each other, via a pivot table.
Has One Through
Reach one distant record through an intermediate model.
Has One of Many
Retrieve a single record (latest/oldest/max) from a hasMany set.
Has Many Through
Reach many distant records through an intermediate model.
Polymorphic 1:1
One table serves as child for multiple parent model types.
Polymorphic 1:N
One table serves as the "many" side for multiple parent types.
Polymorphic M:N
A morph pivot table connects multiple model types to a shared model.
2. One to One — hasOne / belongsTo
A one-to-one relationship means one record in Table A is associated with exactly one record in Table B. The foreign key lives on the child table (the "belongs to" side).
Example: A User has one Profile. The profiles table holds the user_id foreign key.
Database Schema
| users |
profiles |
id — Primary Key |
id — Primary Key |
name |
user_id — Foreign Key → users.id |
email |
bio, avatar, phone |
created_at, updated_at |
created_at, updated_at |
Migration
PHP — Migration
// database/migrations/xxxx_create_users_table.php
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}
// database/migrations/xxxx_create_profiles_table.php
public function up(): void
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('bio')->nullable();
$table->string('avatar')->nullable();
$table->string('phone')->nullable();
$table->timestamps();
});
}
Models (Laravel 12/13 typed style)
PHP — app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
protected $fillable = ['name', 'email'];
/**
* User has ONE profile.
* FK: profiles.user_id → users.id
*/
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
// Override keys if needed:
// return $this->hasOne(Profile::class, 'user_id', 'id');
}
}
PHP — app/Models/Profile.php
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Profile extends Model
{
protected $fillable = ['user_id', 'bio', 'avatar', 'phone'];
/**
* Profile BELONGS TO one user. (Inverse side)
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Controller — All Operations
PHP — Controller Usage
// ── READ ────────────────────────────────────────────────────
$user = User::find(1);
$profile = $user->profile; // dynamic property → runs query once
echo $profile->bio;
// Eager load (avoids N+1 problem when looping many users)
$users = User::with('profile')->get();
// ── CREATE ───────────────────────────────────────────────────
$user->profile()->create([
'bio' => 'Full-stack Laravel developer',
'avatar' => 'avatar.png',
]);
// ── UPDATE ───────────────────────────────────────────────────
$user->profile()->update(['bio' => 'Updated bio']);
// ── DELETE ───────────────────────────────────────────────────
$user->profile->delete();
// ── INVERSE: from profile back to user ───────────────────────
$profile = Profile::find(1);
echo $profile->user->name; // "John Doe"
✅ Best Practice — Always Eager Load When you loop over many users: User::with('profile')->get() runs 2 queries total. Without it: looping over 100 users runs 101 queries vs 2.
3. One to Many — hasMany / belongsTo
One record in the parent table can have many related records in the child table. The foreign key still lives on the child table. This is the most commonly used relationship in Laravel applications.
Example: A Post has many Comments. A User has many Orders.
Database Schema
| posts |
comments |
id PK |
id PK |
user_id FK |
post_id FK → posts.id |
title, body |
user_id FK (commenter) |
status |
body, approved |
Migration
PHP — Migration
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->longText('body');
$table->string('status')->default('draft');
$table->timestamps();
});
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained();
$table->text('body');
$table->boolean('approved')->default(false);
$table->timestamps();
});
Models
PHP — app/Models/Post.php
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
protected $fillable = ['user_id', 'title', 'body', 'status'];
/** Post HAS MANY comments */
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
/** Post BELONGS TO a user (author) */
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
class Comment extends Model
{
protected $fillable = ['post_id', 'user_id', 'body', 'approved'];
/** Comment BELONGS TO one post */
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Controller Usage
PHP — Controller
// ── READ ─────────────────────────────────────────────────────
$post = Post::find(1);
$comments = $post->comments; // Collection of Comment models
// Chain conditions using the method (not property)
$approved = $post->comments()
->where('approved', true)
->latest()
->get();
// Count without loading all records
$count = $post->comments()->count();
// Eager load (prevents N+1)
$posts = Post::with('comments')->get();
// Nested eager loading
$posts = Post::with('comments.user')->get();
// ── CREATE ───────────────────────────────────────────────────
$post->comments()->create([
'user_id' => auth()->id(),
'body' => 'Great article!',
]);
// ── whereHas — filter parents by child condition ──────────────
$postsWithApproved = Post::whereHas('comments', function ($q) {
$q->where('approved', true);
})->get();
// withCount — add comment count to each post
$posts = Post::withCount('comments')->get();
echo $posts[0]->comments_count;
4. Many to Many — belongsToMany
A many-to-many relationship requires a third pivot table that stores the foreign keys of both models. Neither side holds a foreign key directly — they both reference the pivot.
Example: A User can have many Roles. A Role can belong to many Users. The pivot table is role_user.
⚠️ Pivot Table Naming Convention The pivot table name must be the two model names in alphabetical order, singular, snake_case: role_user (not user_role). Laravel derives this automatically. You can override it as the second argument of belongsToMany().
Migration
PHP — Migration
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// Pivot: role_user (alphabetical: r before u)
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->primary(['user_id', 'role_id']); // composite PK
$table->timestamp('assigned_at')->nullable(); // extra pivot column
});
Models
PHP — Models
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at') // expose extra column
->withTimestamps(); // if pivot has timestamps
}
}
class Role extends Model
{
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('assigned_at');
}
}
Controller — Attach, Sync, Detach, Toggle
PHP — Controller
$user = User::find(1);
// ── ATTACH — insert into pivot (does NOT remove existing) ─────
$user->roles()->attach(3);
$user->roles()->attach([1, 2, 3]);
$user->roles()->attach(3, ['assigned_at' => now()]); // with extra pivot data
// ── DETACH — remove from pivot ────────────────────────────────
$user->roles()->detach(3); // detach one
$user->roles()->detach(); // detach ALL roles
// ── SYNC — replaces ALL current records with given set ────────
$user->roles()->sync([1, 2, 3]); // user now has only roles 1,2,3
// ── syncWithoutDetaching — add without removing existing ──────
$user->roles()->syncWithoutDetaching([4, 5]);
// ── TOGGLE — attach if missing, detach if present ─────────────
$user->roles()->toggle([1, 3]);
// ── READ — access pivot data ──────────────────────────────────
foreach ($user->roles as $role) {
echo $role->name;
echo $role->pivot->assigned_at; // extra pivot column
}
5. Has One Through — hasOneThrough
hasOneThrough lets you access a single record on a distant model by jumping through an intermediate model. You never manually load the middle model — Eloquent joins through it.
Example: A Mechanic has one Car. That Car has one Owner. So a Mechanic has one Owner through Car.
PHP — app/Models/Mechanic.php
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
class Mechanic extends Model
{
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class, // Final model we want to reach
Car::class, // Intermediate model to pass through
'mechanic_id', // FK on cars table → mechanics
'car_id', // FK on owners table → cars
'id', // Local key on mechanics table
'id' // Local key on cars table
);
}
}
6. Has One of Many — latestOfMany / ofMany
A model can have many related records but you only want one specific record — the latest, oldest, or the one with the max/min value. hasOneOfMany does this in a single efficient query and supports eager loading (unlike calling ->latest()->first() on a hasMany).
PHP — app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
/** All orders */
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/** Only the most recent order */
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}
/** Only the oldest order */
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}
/** Order with the highest total amount */
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('total', 'max');
}
/** Latest shipped order — conditional ofMany */
public function latestShippedOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany(
['created_at' => 'max'],
fn($query) => $query->where('status', 'shipped')
);
}
}
✅ Key Advantage latestOfMany() / ofMany() are proper Eloquent relationships — they support with() eager loading. orders()->latest()->first() is just a query call that cannot be eager-loaded.
7. Has Many Through — hasManyThrough
hasManyThrough provides access to a collection of distant records through an intermediate model. It executes a single JOIN query — you never need to manually load the middle table.
Example: A Country has many Users. Each User has many Posts. Therefore a Country has many Posts through Users.
PHP — app/Models/Country.php
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Country extends Model
{
public function posts(): HasManyThrough
{
return $this->hasManyThrough(
Post::class, // Final model — what we want
User::class, // Intermediate model — how we get there
'country_id', // FK on users table → countries
'user_id', // FK on posts table → users
'id', // Local key on countries
'id' // Local key on users
);
}
}
8. Polymorphic One to One — morphOne / morphTo
A polymorphic one-to-one relationship allows a single child table to serve multiple parent model types without creating a separate table for each. Two special morph columns identify which parent model and which ID the record belongs to.
Example: Both User and Post can each have one Image, all stored in a single images table.
Migration
PHP — Migration
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->string('url');
// morphs() creates TWO columns:
// imageable_id → unsignedBigInteger
// imageable_type → string (stores the model class name)
$table->morphs('imageable');
$table->timestamps();
});
Models
PHP — Models
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
/** Returns the owning model (User OR Post — resolved dynamically) */
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
class User extends Model
{
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
class Post extends Model
{
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
9. Polymorphic One to Many — morphMany / morphTo
A polymorphic one-to-many relationship lets a single child table hold records belonging to many different parent model types. This is the most frequently used polymorphic pattern.
Example: Both Post and Video can have many Comments. All comments live in one comments table.
PHP — Models
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
public function commentable(): MorphTo
{
return $this->morphTo(); // resolves to Post or Video dynamically
}
}
class Post extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Registering morphMap (Important in Laravel 11/12/13)
PHP — app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Relations\Relation;
public function boot(): void
{
// Instead of storing "App\Models\Post" in DB, store short alias "post"
// Makes DB portable if you rename/move model classes
Relation::morphMap([
'post' => Post::class,
'video' => Video::class,
'comment' => Comment::class,
]);
}
10. Polymorphic Many to Many — morphToMany / morphedByMany
A polymorphic many-to-many uses a morph pivot table to connect multiple model types to a shared model on a many-to-many basis. One morph pivot table handles all model types.
Example: Both Post and Video can have many Tags. A Tag can belong to many Posts and Videos. All connections stored in one taggables table.
Migration
PHP — Migration
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// Morph pivot table — named taggables (model + 'ables')
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable'); // taggable_id + taggable_type
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
});
Models
PHP — Models
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\MorphedByMany;
class Tag extends Model
{
/** All posts that have this tag */
public function posts(): MorphedByMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
/** All videos that have this tag */
public function videos(): MorphedByMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
class Post extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
class Video extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
11. Summary Reference Table
| Relationship |
Parent Method |
Inverse Method |
Returns |
FK Lives On |
Example |
| One to One |
hasOne() |
belongsTo() |
Single model |
Child table |
User → Profile |
| One to Many |
hasMany() |
belongsTo() |
Collection |
Child table |
Post → Comments |
| Many to Many |
belongsToMany() |
belongsToMany() |
Collection |
Pivot table |
User ↔ Roles |
| Has One Through |
hasOneThrough() |
— |
Single model |
Intermediate + distant |
Mechanic → Owner via Car |
| Has One of Many |
hasOne()->latestOfMany() |
— |
Single model |
Child table |
User → Latest Order |
| Has Many Through |
hasManyThrough() |
— |
Collection |
Intermediate + distant |
Country → Posts via Users |
| Poly One to One |
morphOne() |
morphTo() |
Single model |
Morph cols on child |
User/Post → Image |
| Poly One to Many |
morphMany() |
morphTo() |
Collection |
Morph cols on child |
Post/Video → Comments |
| Poly Many to Many |
morphToMany() |
morphedByMany() |
Collection |
Morph pivot table |
Post/Video ↔ Tags |
12. Quiz — Test Your Knowledge
🧠 Section A — Basics (One to One & One to Many)
- In a
hasOne relationship, which table holds the foreign key — the parent or the child table?
- What is the difference between
$user->profile and $user->profile() in Eloquent?
- You have
Post hasMany Comment. Write the Eloquent call to get all approved comments ordered newest first, without loading all comments.
- What Laravel problem does eager loading (
with()) solve? Show lazy vs eager example.
- What default foreign key name does Eloquent assume for
Post::belongsTo(User::class)?
- How do you use
withCount() to add a comment count to each post without loading all comments?
- What does
cascadeOnDelete() do in a migration and when should you use it?
🧠 Section B — Many to Many & Pivot
- What naming convention must the pivot table follow for Eloquent to auto-detect it in
belongsToMany?
- Explain the difference between
attach(), sync(), syncWithoutDetaching(), and toggle().
- How do you store and read extra columns (e.g.
assigned_at) on a pivot table?
- Write the full model and migration for a
Student that belongs to many Courses, with a pivot column enrolled_at.
- If you call
$user->roles()->sync([1, 2, 3]) and the user previously had roles [2, 4, 5], what is the final state?
- How do you filter users who have at least one role with slug
'admin' using whereHas?
🧠 Section C — Through Relationships & Has One of Many
- When would you choose
hasManyThrough over manually loading the intermediate model?
- What is the key advantage of
hasOne()->latestOfMany() over hasMany()->latest()->first()?
- Write the model method to get a User's most expensive
Order (highest total value).
- Explain all 6 parameters of
hasManyThrough() in plain English with an example.
- Can you use
with('latestOrder') for eager loading when defined via hasOne()->latestOfMany()? Why or why not?
🧠 Section D — Polymorphic Relationships
- What two database columns does
$table->morphs('commentable') create, and what does each store?
- What is
Relation::morphMap() and why should you always register it in AppServiceProvider::boot()?
- Explain the difference between
morphOne, morphMany, and morphToMany.
- Write the full
Tag model with posts() and videos() inverse relationships using morphedByMany.
- When should you use a polymorphic relationship instead of creating separate tables for each parent type? Give a real-world scenario.
🧠 Section E — Advanced / Gotchas
- What happens if you call
$post->comments inside a foreach loop over 100 posts without eager loading?
- What is the difference between
whereHas('comments') and with('comments')?
- Can you chain query builder conditions on a relationship result like
$post->comments()->where(...)->get()? Explain why.
- How do you eager-load a nested relationship like comments with their authors on a list of posts?
- If you define
public function latestOrder(): HasOne using latestOfMany(), what class must the return type hint be — HasOne or HasMany?
- You have a polymorphic
likes table where both Post and Comment can be liked. Write the full migration, Like model, and Post model relationship.
Comments (0)
Leave a Comment