What Are We Doing in This Post?
Our blog has users and posts stored in the database. The posts table has a user_id column that references the users table. But in Eloquent, we have not told our models about this connection yet.
Right now if you want the author's name on a post, you would have to manually query the users table with the user_id. That is repetitive and messy.
Eloquent Relationships let you define these connections directly on the model — then access related data as simple properties. $post->user->name instead of a manual query. Clean, readable, and powerful.
This episode covers the four most important relationship types with real examples on our blog.
What is an Eloquent Relationship?
A relationship tells Eloquent how two models are connected through the database.
Real world analogy: Think of a library. A book belongs to one author. An author has many books. These are real-world relationships. Eloquent lets you describe these same relationships in code — and once described, navigating between related records is as simple as accessing a property.
Relationship 1 — belongsTo
A post belongs to one user. This is defined on the Post model — the model that holds the foreign key (user_id).
Update app/Models/Post.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model { protected $fillable = [ 'user_id', 'title', 'slug', 'body', 'status', 'views', 'published_at', ];
protected $casts = [ 'published_at' => 'datetime', ];
public function user() { return $this->belongsTo(User::class); } }
belongsTo(User::class) tells Eloquent: this Post belongs to one User. Eloquent automatically uses user_id as the foreign key because the method is named user — it takes the method name, appends _id, and looks for that column. Convention over configuration from Episode 14 in action.
Now anywhere you have a Post object you can access the author:
<?php
$post = Post::findOrFail(1);
echo $post->user->name; echo $post->user->email;
Eloquent automatically runs the join query behind the scenes. You access it like a simple property.
Relationship 2 — hasMany
A user has many posts. This is the other side of the same relationship, defined on the User model.
Update app/Models/User.php — add the posts relationship method. Find the class body and add this method:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable;
class User extends Authenticatable { use HasFactory, Notifiable;
protected $fillable = [ 'name', 'email', 'password', ];
protected $hidden = [ 'password', 'remember_token', ];
protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; }
public function posts() { return $this->hasMany(Post::class); } }
hasMany(Post::class) tells Eloquent: this User has many Posts. Eloquent automatically uses user_id as the foreign key on the posts table.
Now you can fetch all posts by a user:
<?php
$user = User::findOrFail(1);
$posts = $user->posts;
foreach ($posts as $post) { echo $post->title; }
$user->posts returns a Collection of all Post models belonging to that user. No manual WHERE clause needed.
You can also chain query builder methods on relationships:
<?php
$published_posts = $user->posts()->where('status', 'published')->latest()->get();
$post_count = $user->posts()->count();
Notice the difference — $user->posts (no parentheses) returns the Collection directly. $user->posts() (with parentheses) returns the query builder so you can chain additional conditions before executing.
Relationship 3 — hasOne
A hasOne relationship means a model has exactly one related model.
For our blog, let us say each user has one profile with extra information — bio, website, avatar.
Create the migration:
php artisan make:migration create_profiles_table
Update the migration file:
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('profiles', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->unique()->constrained()->onDelete('cascade'); $table->string('bio')->nullable(); $table->string('website')->nullable(); $table->string('avatar')->nullable(); $table->string('location')->nullable(); $table->timestamps(); }); }
public function down(): void { Schema::dropIfExists('profiles'); } };
->unique() on the foreign key enforces that each user can only have one profile row — that is what makes this a one-to-one relationship at the database level.
Create the Profile model:
php artisan make:model Profile
Update app/Models/Profile.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Profile extends Model { protected $fillable = [ 'user_id', 'bio', 'website', 'avatar', 'location', ];
public function user() { return $this->belongsTo(User::class); } }
Add the hasOne relationship to the User model:
<?php
public function profile() { return $this->hasOne(Profile::class); }
Now accessing a user's profile is one property:
<?php
$user = User::findOrFail(1);
echo $user->profile->bio; echo $user->profile->website;
If the profile does not exist yet, $user->profile returns null — so always check with $user->profile?->bio or @isset($user->profile) in Blade.
Run the migration:
php artisan migrate
Relationship 4 — belongsToMany
A many-to-many relationship means both sides can have multiple of the other. Posts can have many tags. A tag can belong to many posts.
This requires a pivot table — a middle table that stores the connections between the two models.
Create the tags migration and pivot table migration together:
php artisan make:migration create_tags_table php artisan make:migration create_post_tag_table
Update the tags migration:
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('tags', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); $table->string('slug')->unique(); $table->timestamps(); }); }
public function down(): void { Schema::dropIfExists('tags'); } };
Update the pivot table migration:
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('post_tag', function (Blueprint $table) { $table->foreignId('post_id')->constrained()->onDelete('cascade'); $table->foreignId('tag_id')->constrained()->onDelete('cascade'); $table->primary(['post_id', 'tag_id']); }); }
public function down(): void { Schema::dropIfExists('post_tag'); } };
The pivot table naming convention is alphabetical singular model names separated by underscore — post_tag not tag_post and not posts_tags. Laravel finds it automatically using this convention.
->primary(['post_id', 'tag_id']) creates a composite primary key — a combination of both columns must be unique. This prevents the same tag being attached to the same post twice.
Create the Tag model:
php artisan make:model Tag
Update app/Models/Tag.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model { protected $fillable = [ 'name', 'slug', ];
public function posts() { return $this->belongsToMany(Post::class); } }
Add the tags relationship to app/Models/Post.php:
<?php
public function tags() { return $this->belongsToMany(Tag::class); }
Now you can attach and detach tags from posts and access them as a collection:
<?php
$post = Post::findOrFail(1);
$post->tags()->attach([1, 2, 3]);
$post->tags()->detach(2);
$post->tags()->sync([1, 3]);
foreach ($post->tags as $tag) { echo $tag->name; }
$tag = Tag::findOrFail(1); foreach ($tag->posts as $post) { echo $post->title; }
attach() adds tag IDs to the pivot table. detach() removes them. sync() is the most powerful — it takes an array of IDs and makes the pivot table match exactly. Extra IDs are removed, missing IDs are added. One method call replaces the entire set of tags on a post.
Run the new migrations:
php artisan migrate
Eager Loading — Solving the N+1 Problem
This is one of the most important performance concepts in Eloquent. Understand it now and save yourself hours of debugging slow applications later.
Consider this code in PostController@index:
<?php
$posts = Post::latest()->get();
Then in the view:
@foreach($posts as $post) <p>{{ $post->user->name }}</p> @endforeach
This looks innocent. But here is what happens at the database level. Laravel runs one query to fetch all posts. Then for every single post in the loop, it runs another separate query to fetch that post's user. If you have 50 posts, that is 51 database queries — 1 for posts plus 50 for users.
This is called the N+1 problem. N queries for the related data plus 1 for the original. It kills performance on any real application.
The fix — eager loading with with():
<?php
$posts = Post::with('user')->latest()->get();
Now Laravel runs exactly two queries — one to fetch all posts and one to fetch all the related users at once. It then matches them up in memory. No matter how many posts you have, it is always exactly two queries.
Always eager load relationships that you will access in a loop.
You can eager load multiple relationships at once by passing an array to with().
Update the Application to Use Relationships
Update PostController@index to eager load the user relationship:
<?php
public function index() { $posts = Post::with('user')->latest()->get(); return view('posts.index', ['posts' => $posts]); }
Update PostController@show to eager load user and tags:
<?php
public function show($id) { $post = Post::with(['user', 'tags'])->findOrFail($id); $post->increment('views'); return view('posts.show', ['post' => $post]); }
Update resources/views/posts/index.blade.php — add author name to each post card. Find the post-meta div and update it:
<div class="post-meta"> <span>By {{ $post->user?->name ?? 'Unknown' }}</span> <span>{{ $post->created_at->diffForHumans() }}</span> <span>{{ $post->views }} views</span> <a href="{{ route('posts.show', $post->id) }}" class="read-more">Read More →</a> </div>
$post->user?->name uses the nullsafe operator — if $post->user is null (post has no associated user), it returns null instead of throwing an error. The ?? 'Unknown' provides a fallback.
Update resources/views/posts/show.blade.php — add author and tags. Update the meta section:
<div style="display:flex; gap:16px; color:#94a3b8; font-size:13px; margin-bottom:32px; padding-bottom:24px; border-bottom:1px solid #e2e8f0; flex-wrap:wrap;"> <span>By <strong style="color:#475569;">{{ $post->user?->name ?? 'Unknown' }}</strong></span> <span>{{ $post->created_at->format('d M Y') }}</span> <span>{{ $post->views }} views</span> </div>
@if($post->tags->isNotEmpty()) <div style="margin-bottom:24px; display:flex; gap:8px; flex-wrap:wrap;"> @foreach($post->tags as $tag) <span style="background:#f1f5f9; color:#475569; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600;"> {{ $tag->name }} </span> @endforeach </div> @endif
$post->tags->isNotEmpty() is a Collection method — returns true if the collection has at least one item. Cleaner than count($post->tags) > 0.
Seeding Tags and Relationships
Let us add some tags to our database. Create a tag seeder:
php artisan make:seeder TagSeeder
Update database/seeders/TagSeeder.php:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder; use App\Models\Tag; use App\Models\Post; use Illuminate\Support\Str;
class TagSeeder extends Seeder { public function run(): void { $tags = ['Laravel', 'PHP', 'JavaScript', 'Database', 'Tutorial', 'Beginner', 'Backend'];
foreach ($tags as $tagName) { Tag::create([ 'name' => $tagName, 'slug' => Str::slug($tagName), ]); }
$posts = Post::all();
foreach ($posts as $index => $post) { $tagIds = Tag::inRandomOrder()->take(rand(2, 4))->pluck('id')->toArray(); $post->tags()->sync($tagIds); } } }
Update database/seeders/DatabaseSeeder.php:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder { public function run(): void { $this->call([ PostSeeder::class, TagSeeder::class, ]); } }
Run a fresh migration with seeding:
php artisan migrate:fresh --seed
Now visit http://127.0.0.1:8000/posts — each post card shows its author. Click any post — you see the author name and tags displayed below the title.
Quick Reference — All Relationship Methods
<?php
$post->user; $post->user->name;
$user->posts; $user->posts()->where('status', 'published')->get(); $user->posts()->count();
$user->profile; $user->profile?->bio;
$post->tags; $post->tags()->attach([1, 2]); $post->tags()->detach(1); $post->tags()->sync([2, 3]);
Post::with('user')->get(); Post::with(['user', 'tags'])->get(); User::with('posts.tags')->get();
User::with('posts.tags') is nested eager loading — load all users with their posts, and for each post also load its tags. All in three total queries regardless of data size.
What Did We Learn in This Post?
belongsTo is defined on the model holding the foreign key — a Post belongs to a User.
hasMany is the inverse — a User has many Posts. Access as a property for a Collection, as a method for a query builder.
hasOne defines a one-to-one relationship — a User has one Profile. The foreign key unique constraint enforces this at the database level.
belongsToMany handles many-to-many through a pivot table. attach() adds, detach() removes, sync() replaces the entire set.
The N+1 problem occurs when related data is accessed inside a loop without eager loading. Fix it with with() — always eager load relationships used in views.
$collection->isNotEmpty() is cleaner than count checks. The nullsafe operator ?-> prevents errors when a relationship returns null.
What is Coming in Episode 23?
Our blog application is now complete with authentication, relationships, and full CRUD. It is time to wrap up the course with the final real-world project episode.
Episode 23 covers deploying your Laravel application to a live server — setting up a production environment, configuring environment variables, running migrations on the server, and making your application accessible to the entire world.
See you in the next one.
Next Episode: Deploying Laravel — Taking Your Application Live
This is Episode 22 of the PHP and Laravel — Zero to Hero series.
No comments:
Post a Comment