Model Relationships

Module 19: PHP Backend - Laravel

Introduction to Eloquent Relationships

Database tables are rarely isolated; they connect to other tables to form a complete data model. Eloquent relationships provide an elegant way to define these connections between models, making it easy to traverse and manipulate related data.

Think of relationships as the pathways between different entities in your application, similar to how highways connect cities in a road network. Just as we have different types of roads (highways, local roads, one-way streets), Eloquent provides various relationship types for different connection patterns.

classDiagram class User { +id +name +email +posts() +profile() +roles() } class Post { +id +title +content +user_id +user() +comments() +tags() } class Profile { +id +bio +avatar +user_id +user() } class Comment { +id +content +post_id +user_id +post() +user() } class Role { +id +name +users() } class Tag { +id +name +posts() } User "1" --> "many" Post : hasMany User "1" --> "1" Profile : hasOne User "many" --> "many" Role : belongsToMany Post "1" --> "many" Comment : hasMany Post "many" --> "many" Tag : belongsToMany Profile "1" --> "1" User : belongsTo Comment "many" --> "1" Post : belongsTo Comment "many" --> "1" User : belongsTo

This diagram shows common relationship types in a typical blog system. By defining these relationships in your models, you gain powerful capabilities to traverse, retrieve, and manipulate related data in an object-oriented way.

One-to-One Relationships

A one-to-one relationship is the simplest form of relationship, where one record in the first table is associated with exactly one record in the second table, and vice versa.

Examples in real applications include:

erDiagram USERS ||--|| PROFILES : has USERS { id int PK name string email string } PROFILES { id int PK user_id int FK bio text avatar string }

Defining a One-to-One Relationship


// User model (the parent)
public function profile()
{
    return $this->hasOne(Profile::class);
}

// Profile model (the child)
public function user()
{
    return $this->belongsTo(User::class);
}
            

In this example:

Customizing the Foreign Key


// Custom foreign key
public function profile()
{
    return $this->hasOne(Profile::class, 'user_reference_id');
}

// Custom foreign key and local key
public function profile()
{
    return $this->hasOne(Profile::class, 'user_reference_id', 'uuid');
}
            

Working with One-to-One Relationships


// Accessing the related model
$profile = $user->profile;

// Creating a related model
$user->profile()->create([
    'bio' => 'Laravel developer',
    'avatar' => 'avatar.jpg'
]);

// Updating a related model
$user->profile()->update([
    'bio' => 'Senior Laravel developer'
]);

// Checking for the existence of a relationship
if ($user->profile) {
    // User has a profile
}

// Eager loading to avoid N+1 problem
$users = User::with('profile')->get();
foreach ($users as $user) {
    echo $user->profile->bio; // No additional queries
}
            

One-to-one relationships are like exclusive private lines between entities - each user has their own direct connection to a single profile. It's similar to how each person typically has one passport or one driver's license.

One-to-Many Relationships

A one-to-many relationship occurs when a single record in the first table can be associated with multiple records in the second table, but each record in the second table is associated with only one record in the first table.

Examples in real applications include:

erDiagram USERS ||--o{ POSTS : writes USERS { id int PK name string email string } POSTS { id int PK user_id int FK title string content text }

Defining a One-to-Many Relationship


// User model (the parent)
public function posts()
{
    return $this->hasMany(Post::class);
}

// Post model (the child)
public function user()
{
    return $this->belongsTo(User::class);
}
            

In this example:

Working with One-to-Many Relationships


// Accessing the related models
$posts = $user->posts; // Collection of Post models

// Creating a related model
$user->posts()->create([
    'title' => 'Learning Laravel Relationships',
    'content' => 'Relationships are a powerful feature...'
]);

// Using the relationship as a query builder
$recentPosts = $user->posts()
    ->where('created_at', '>=', now()->subDays(7))
    ->get();

// Counting related models
$postCount = $user->posts()->count();

// Eager loading with constraints
$users = User::with(['posts' => function ($query) {
    $query->where('published', true)->orderBy('created_at', 'desc');
}])->get();

// Filtering models based on relationship existence
$usersWithPosts = User::has('posts')->get();
$usersWithRecentPosts = User::whereHas('posts', function ($query) {
    $query->where('created_at', '>=', now()->subDays(7));
})->get();
            

One-to-many relationships are like a hub-and-spoke system - one central entity connects to many related entities. It's similar to how one teacher might have many students, or how a parent might have multiple children.

Many-to-Many Relationships

A many-to-many relationship occurs when multiple records in the first table can be associated with multiple records in the second table. This requires a pivot (or junction) table to manage the associations.

Examples in real applications include:

erDiagram POSTS ||--o{ POST_TAG : has TAGS ||--o{ POST_TAG : belongs_to POSTS { id int PK title string content text } TAGS { id int PK name string } POST_TAG { post_id int FK tag_id int FK }

Defining a Many-to-Many Relationship


// Post model
public function tags()
{
    return $this->belongsToMany(Tag::class);
}

// Tag model
public function posts()
{
    return $this->belongsToMany(Post::class);
}
            

In this example:

Customizing the Pivot Table


// Custom pivot table name
public function roles()
{
    return $this->belongsToMany(Role::class, 'user_roles');
}

// Custom pivot table with custom keys
public function roles()
{
    return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
}
            

Working with Many-to-Many Relationships


// Accessing the related models
$tags = $post->tags; // Collection of Tag models

// Attaching tags to a post
$post->tags()->attach([1, 2, 3]); // Attach by IDs
$post->tags()->attach($tagModels); // Attach model instances

// Detaching tags from a post
$post->tags()->detach([2, 3]); // Detach specific tags
$post->tags()->detach(); // Detach all tags

// Syncing tags (removes existing associations and adds new ones)
$post->tags()->sync([1, 4, 5]);

// Toggle tags (adds missing and removes existing)
$post->tags()->toggle([1, 2, 3]);

// Querying based on pivot columns
$post->tags()->wherePivot('featured', true)->get();

// Defining default ordering
public function tags()
{
    return $this->belongsToMany(Tag::class)->orderBy('name');
}
            

Pivot Table with Additional Columns

Often, a pivot table needs to store additional information about the relationship itself:


// Migration for a pivot table with additional columns
Schema::create('course_student', function (Blueprint $table) {
    $table->id();
    $table->foreignId('course_id')->constrained();
    $table->foreignId('student_id')->constrained('users');
    $table->date('enrolled_at');
    $table->integer('grade')->nullable();
    $table->timestamps();
});

// Model definition
public function courses()
{
    return $this->belongsToMany(Course::class)
        ->withPivot('enrolled_at', 'grade')
        ->withTimestamps();
}

// Attaching with additional data
$student->courses()->attach($course->id, [
    'enrolled_at' => now(),
]);

// Accessing pivot data
foreach ($student->courses as $course) {
    echo $course->pivot->enrolled_at;
    echo $course->pivot->grade;
}

// Updating pivot data
$student->courses()->updateExistingPivot($course->id, [
    'grade' => 95,
]);
            

Custom Pivot Models

For more complex pivot relationships, you can create a dedicated pivot model:


// Custom pivot model
class Enrollment extends Pivot
{
    protected $table = 'course_student';
    
    // Additional methods or relationships
    public function isPassing()
    {
        return $this->grade >= 60;
    }
}

// Using the custom pivot model
public function courses()
{
    return $this->belongsToMany(Course::class)
        ->using(Enrollment::class)
        ->withPivot('enrolled_at', 'grade')
        ->withTimestamps();
}

// Now you can use the custom methods
foreach ($student->courses as $course) {
    if ($course->pivot->isPassing()) {
        echo 'Passing grade in ' . $course->name;
    }
}
            

Many-to-many relationships are like networking events where attendees can connect with multiple other attendees. Each connection (stored in the pivot table) can also have its own attributes (like when they met, what they discussed, etc.).

Has One Through / Has Many Through

These relationships provide convenient shortcuts to access distant relations through an intermediate model.

Has One Through

A has-one-through relationship links models through a single intermediate relationship.

erDiagram SUPPLIERS ||--|| USERS : has USERS ||--|| HISTORY : has SUPPLIERS { id int PK name string } USERS { id int PK supplier_id int FK name string } HISTORY { id int PK user_id int FK notes text }

// A supplier has one history through a user
class Supplier extends Model
{
    public function history()
    {
        return $this->hasOneThrough(
            History::class,  // Target model
            User::class,     // Intermediate model
            'supplier_id',   // Foreign key on intermediate
            'user_id',       // Foreign key on target
            'id',            // Local key on this model
            'id'             // Local key on intermediate
        );
    }
}

// Usage
$notes = $supplier->history->notes;
            

Has Many Through

A has-many-through relationship links models through a single intermediate relationship, but returns multiple models.

erDiagram COUNTRIES ||--o{ USERS : has USERS ||--o{ POSTS : writes COUNTRIES { id int PK name string } USERS { id int PK country_id int FK name string } POSTS { id int PK user_id int FK title string }

// A country has many posts through users
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,     // Target model
            User::class,     // Intermediate model
            'country_id',    // Foreign key on intermediate
            'user_id',       // Foreign key on target
            'id',            // Local key on this model
            'id'             // Local key on intermediate
        );
    }
}

// Usage
$posts = $country->posts; // All posts from users in this country
            

Think of these "through" relationships as creating shortcut paths in a network. Instead of having to traverse from Country → Users → Posts manually, the has-many-through relationship creates a direct path from Country to Posts, similar to how an express train skips intermediate stations.

Polymorphic Relationships

Polymorphic relationships allow a model to belong to more than one type of model using a single association. This is particularly useful when you have multiple models that might share the same type of relationship.

One-to-One Polymorphic

For example, a model might have one current image that could belong to either a user, a post, or a product:

erDiagram USERS ||--o| IMAGES : has POSTS ||--o| IMAGES : has PRODUCTS ||--o| IMAGES : has USERS { id int PK name string } POSTS { id int PK title string } PRODUCTS { id int PK name string } IMAGES { id int PK imageable_id int FK imageable_type string url string }

// Migration for images table
Schema::create('images', function (Blueprint $table) {
    $table->id();
    $table->string('url');
    $table->unsignedBigInteger('imageable_id');
    $table->string('imageable_type');
    $table->timestamps();
});

// Image model
class Image extends Model
{
    public function imageable()
    {
        return $this->morphTo();
    }
}

// User model
class User extends Model
{
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

// Post model
class Post extends Model
{
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

// Usage
$user->image; // Get the user's image
$post->image; // Get the post's image

$user->image()->create(['url' => 'avatars/john.jpg']);
$post->image()->create(['url' => 'posts/header.jpg']);

// From the image, get the parent
$image = Image::find(1);
$parent = $image->imageable; // Could be a User, Post, etc.
            

One-to-Many Polymorphic

Similar to one-to-one polymorphic, but each parent can have multiple children:


// Comment model
class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

// Post model
class Post extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Video model
class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// Usage
$post->comments; // All comments on this post
$video->comments; // All comments on this video

// Adding comments
$post->comments()->create([
    'content' => 'Great post!',
    'user_id' => 1
]);

// Finding the parent
$comment = Comment::find(1);
$parent = $comment->commentable; // Either a Post or Video
            

Many-to-Many Polymorphic

For scenarios where multiple types of models can be associated with multiple instances of another model:

erDiagram POSTS ||--o{ TAGGABLES : has VIDEOS ||--o{ TAGGABLES : has TAGS ||--o{ TAGGABLES : belongs_to POSTS { id int PK title string } VIDEOS { id int PK title string } TAGS { id int PK name string } TAGGABLES { tag_id int FK taggable_id int FK taggable_type string }

// Migrations
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});

Schema::create('taggables', function (Blueprint $table) {
    $table->unsignedBigInteger('tag_id');
    $table->unsignedBigInteger('taggable_id');
    $table->string('taggable_type');
    $table->timestamps();
    
    $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});

// Tag model
class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
    
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

// Post model
class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

// Video model
class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

// Usage
$post->tags; // All tags for this post
$video->tags; // All tags for this video

$post->tags()->attach($tagId);
$video->tags()->attach($tagId);

// From the tag, get related models
$tag = Tag::find(1);
$tag->posts; // All posts with this tag
$tag->videos; // All videos with this tag
            

Polymorphic relationships are like multipurpose adapters that can connect to different types of devices. They provide flexibility when you have common functionality (like commenting, tagging, or image attachments) that needs to work across different model types.

Advanced Relationship Techniques

Relationship Existence Queries

Filtering models based on the existence of related models:


// Users who have posts
$users = User::has('posts')->get();

// Users who have 3 or more posts
$users = User::has('posts', '>=', 3)->get();

// Users who have posts with specific conditions
$users = User::whereHas('posts', function ($query) {
    $query->where('published', true);
})->get();

// Users who don't have posts
$users = User::doesntHave('posts')->get();

// Users who have posts but none that are published
$users = User::whereDoesntHave('posts', function ($query) {
    $query->where('published', true);
})->get();

// Count related models in a column
$users = User::withCount('posts')->get();
foreach ($users as $user) {
    echo $user->posts_count; // Number of posts
}

// Count with conditions
$users = User::withCount([
    'posts',
    'posts as published_posts_count' => function ($query) {
        $query->where('published', true);
    }
])->get();

// Conditional relationships
$users = User::with(['posts' => function ($query) {
    $query->where('published', true);
}])->get();
            

Eager Loading

Eager loading is a technique to load all related models in a single query, avoiding the N+1 query problem:

sequenceDiagram participant C as Client participant A as Application participant DB as Database Note over C,DB: Without Eager Loading (N+1 Problem) C->>A: Get all posts with authors A->>DB: SELECT * FROM posts DB->>A: Return 100 posts loop For each post A->>DB: SELECT * FROM users WHERE id = ? DB->>A: Return user end A->>C: Return posts with authors Note over C,DB: With Eager Loading (2 Queries) C->>A: Get all posts with authors A->>DB: SELECT * FROM posts DB->>A: Return 100 posts A->>DB: SELECT * FROM users WHERE id IN (1,2,3,...,N) DB->>A: Return all relevant users A->>C: Return posts with authors

// Basic eager loading
$posts = Post::with('user')->get();

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

// Nested relationships
$posts = Post::with(['user.profile', 'comments.user'])->get();

// Eager loading with constraints
$posts = Post::with(['comments' => function ($query) {
    $query->orderBy('created_at', 'desc')
          ->take(5); // Only load 5 most recent comments
}])->get();

// Lazy eager loading (add relationships after initial query)
$posts = Post::all();
if ($includeComments) {
    $posts->load('comments');
}

// Conditional eager loading
$posts = Post::with(['user' => function ($query) {
    $query->where('type', 'author');
}])->get();
            

Nested Eager Loading with Conditions


// Load posts with recent comments by active users
$posts = Post::with([
    'comments' => function ($query) {
        $query->where('created_at', '>=', now()->subDays(30))
              ->orderBy('created_at', 'desc');
    },
    'comments.user' => function ($query) {
        $query->where('active', true);
    }
])->get();
            

Eager loading is like preparing a shopping list before going to the store - instead of making separate trips for each item (individual queries), you get everything you need in one or two efficient visits.

Lazy Loading vs. Eager Loading


// Lazy loading (N+1 problem)
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name; // Separate query for each post's user
}

// Eager loading (2 queries total)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
    echo $post->user->name; // No additional queries
}
            

Eager loading significantly improves performance when you're working with collections of models and their relationships.

Query Relationships with Constraints

Relationship Methods vs. Dynamic Properties

You can access relationships as dynamic properties or as methods:


// As a property (gets all posts)
$posts = $user->posts;

// As a method (returns a query builder)
$posts = $user->posts()->where('published', true)->get();
            

Constraining Eager Loads


// Load only published posts
$users = User::with(['posts' => function ($query) {
    $query->where('published', true);
}])->get();

// Load latest post and its comments
$users = User::with([
    'posts' => function ($query) {
        $query->latest()->take(1);
    },
    'posts.comments' => function ($query) {
        $query->orderBy('created_at', 'desc');
    }
])->get();

// More complex constraints
$users = User::with([
    'posts' => function ($query) {
        $query->whereBetween('created_at', [
            now()->subMonth(),
            now()
        ])->where('published', true);
    }
])->get();
            

Ordering Relationships


// Default ordering (always apply this ordering)
public function posts()
{
    return $this->hasMany(Post::class)->orderBy('created_at', 'desc');
}

// Ordering eager loaded relationships
$users = User::with(['posts' => function ($query) {
    $query->orderBy('title');
}])->get();
            

These techniques are like having a personalized filtering system for your data - you get exactly what you need, saving both processing time and bandwidth.

Inserting & Updating Related Models

The save Method


// Create a new related model
$post = new Post([
    'title' => 'New Post',
    'content' => 'Content...'
]);

$user->posts()->save($post);

// Create multiple related models
$user->posts()->saveMany([
    new Post(['title' => 'Post 1']),
    new Post(['title' => 'Post 2'])
]);
            

The create Method


// Create from array of attributes
$post = $user->posts()->create([
    'title' => 'New Post',
    'content' => 'Content...'
]);

// Create multiple
$user->posts()->createMany([
    ['title' => 'Post 1', 'content' => '...'],
    ['title' => 'Post 2', 'content' => '...']
]);
            

Attaching & Detaching


// Many-to-many relationships
$user->roles()->attach($roleId);
$user->roles()->attach([1, 2, 3]); // Multiple IDs
$user->roles()->attach($roleId, ['expires_at' => now()->addYear()]); // With pivot data

$user->roles()->detach($roleId);
$user->roles()->detach(); // Detach all roles

// Syncing (removes existing then adds new)
$user->roles()->sync([1, 2, 3]);
$user->roles()->sync([ // With pivot data
    1 => ['expires_at' => now()->addYear()],
    2 => ['expires_at' => now()->addMonths(6)]
]);

// Toggle (add missing, remove existing)
$user->roles()->toggle([1, 2, 3]);
            

Updating Many-to-Many Relationships


// Update pivot table data
$user->roles()->updateExistingPivot($roleId, [
    'expires_at' => now()->addYears(2)
]);
            

These manipulation methods make working with related data intuitive and expressive. It's like having a well-organized filing system where you can easily add, remove, or update connected documents.

Real-World Example: E-commerce Relationships

Let's examine how relationships might work in a real e-commerce application:

classDiagram class User { +id +name +email +orders() +reviews() +wishlist() +address() } class Order { +id +user_id +status +total +user() +items() +payments() +shippingAddress() } class OrderItem { +id +order_id +product_id +quantity +price +order() +product() } class Product { +id +name +price +stock +category_id +brand_id +category() +brand() +reviews() +images() +tags() +variants() } class Category { +id +name +parent_id +products() +parent() +children() } class Tag { +id +name +products() } class ProductImage { +id +product_id +url +product() } User "1" --> "many" Order : hasMany User "1" --> "many" Review : hasMany User "many" --> "many" Product : belongsToMany(wishlist) Order "1" --> "many" OrderItem : hasMany Order "many" --> "1" User : belongsTo OrderItem "many" --> "1" Order : belongsTo OrderItem "many" --> "1" Product : belongsTo Product "1" --> "many" OrderItem : hasMany Product "many" --> "1" Category : belongsTo Product "many" --> "1" Brand : belongsTo Product "1" --> "many" Review : hasMany Product "1" --> "many" ProductImage : hasMany Product "many" --> "many" Tag : belongsToMany Category "1" --> "many" Product : hasMany Category "many" --> "1" Category : belongsTo(parent) Category "1" --> "many" Category : hasMany(children)

User Model


class User extends Model
{
    // One-to-Many: A user has many orders
    public function orders()
    {
        return $this->hasMany(Order::class);
    }
    
    // One-to-Many: A user has many reviews
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }
    
    // Many-to-Many: A user can wishlist many products
    public function wishlist()
    {
        return $this->belongsToMany(Product::class, 'wishlist')
            ->withTimestamps();
    }
    
    // Has-One: A user has one primary shipping address
    public function address()
    {
        return $this->hasOne(Address::class)->where('is_primary', true);
    }
    
    // Has-Many: A user has many addresses
    public function addresses()
    {
        return $this->hasMany(Address::class);
    }
}
            

Product Model


class Product extends Model
{
    // Belongs-To: A product belongs to a category
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
    
    // Belongs-To: A product belongs to a brand
    public function brand()
    {
        return $this->belongsTo(Brand::class);
    }
    
    // One-to-Many: A product has many reviews
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }
    
    // Has-Many: A product has many images
    public function images()
    {
        return $this->hasMany(ProductImage::class);
    }
    
    // Many-to-Many: A product can have many tags
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
    
    // Has-Many: A product can have many variants
    public function variants()
    {
        return $this->hasMany(ProductVariant::class);
    }
    
    // Has-One: A product has one primary image
    public function mainImage()
    {
        return $this->hasOne(ProductImage::class)
            ->orderBy('is_primary', 'desc')
            ->orderBy('id', 'asc');
    }
    
    // Many-to-Many: Users who have wishlisted this product
    public function wishlistedBy()
    {
        return $this->belongsToMany(User::class, 'wishlist')
            ->withTimestamps();
    }
    
    // Many through: Get all reviews from this product's category
    public function categorySiblingReviews()
    {
        return $this->hasManyThrough(
            Review::class,
            Product::class,
            'category_id', // Foreign key on intermediate (Product)
            'product_id',  // Foreign key on target (Review)
            'category_id', // Local key on this model (Product)
            'id'           // Local key on intermediate (Product)
        )->where('products.id', '!=', $this->id);
    }
}
            

Category Model


class Category extends Model
{
    // One-to-Many: A category has many products
    public function products()
    {
        return $this->hasMany(Product::class);
    }
    
    // Self-referential Belongs-To: A category may have a parent category
    public function parent()
    {
        return $this->belongsTo(Category::class, 'parent_id');
    }
    
    // Self-referential Has-Many: A category may have child categories
    public function children()
    {
        return $this->hasMany(Category::class, 'parent_id');
    }
    
    // Recursive relationship to get all descendants
    public function descendants()
    {
        return $this->children()->with('descendants');
    }
    
    // Recursive relationship to get all ancestors
    public function ancestors()
    {
        return $this->parent()->with('ancestors');
    }
    
    // Has-Many-Through: Get all products from subcategories
    public function allProducts()
    {
        // Get direct products
        $directProducts = $this->products();
        
        // Get child category IDs
        $childIds = $this->descendants()->pluck('id')->toArray();
        
        // If no children, just return direct products
        if (empty($childIds)) {
            return $directProducts;
        }
        
        // Otherwise, also get products from all child categories
        return Product::where(function ($query) use ($childIds) {
            $query->where('category_id', $this->id)
                  ->orWhereIn('category_id', $childIds);
        });
    }
}
            

Order Model


class Order extends Model
{
    // Belongs-To: An order belongs to a user
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    // Has-Many: An order has many items
    public function items()
    {
        return $this->hasMany(OrderItem::class);
    }
    
    // Has-Many: An order has many payments
    public function payments()
    {
        return $this->hasMany(Payment::class);
    }
    
    // Has-One: An order has one shipping address
    public function shippingAddress()
    {
        return $this->hasOne(OrderAddress::class)
            ->where('type', 'shipping');
    }
    
    // Has-One: An order has one billing address
    public function billingAddress()
    {
        return $this->hasOne(OrderAddress::class)
            ->where('type', 'billing');
    }
    
    // Has-Many-Through: Get all products in this order
    public function products()
    {
        return $this->hasManyThrough(
            Product::class,
            OrderItem::class,
            'order_id',   // Foreign key on OrderItem
            'id',         // Foreign key on Product (actually primary key)
            'id',         // Local key on Order
            'product_id'  // Local key on OrderItem
        );
    }
}
            

OrderItem Model


class OrderItem extends Model
{
    // Belongs-To: An order item belongs to an order
    public function order()
    {
        return $this->belongsTo(Order::class);
    }
    
    // Belongs-To: An order item refers to a product
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
    
    // Accessor to calculate subtotal
    public function getSubtotalAttribute()
    {
        return $this->quantity * $this->price;
    }
}
            

This example demonstrates how a complex application can be modeled using Eloquent relationships. Each model maintains its own specific relationships, creating a web of connections that accurately represents the business domain.

Best Practices for Working with Relationships

Common Pitfalls to Avoid

Practice Activity

Basic Relationships Exercise

Create a simple blog system with the following relationships:

  1. A user has many posts (one-to-many)
  2. A post belongs to a user (belongs-to)
  3. A post has many comments (one-to-many)
  4. A comment belongs to a post and a user (two belongs-to)
  5. A post can have many tags, and a tag can belong to many posts (many-to-many)

Define all the model relationships and create example code to demonstrate retrieving and creating related data.

Advanced Relationships Exercise

Extend the blog system with more advanced relationships:

  1. A user has one profile (one-to-one)
  2. A user can follow many other users, and be followed by many users (self-referential many-to-many)
  3. A post and a comment can both have many likes (polymorphic one-to-many)
  4. A category can have nested subcategories (self-referential one-to-many)
  5. A user can bookmark both posts and comments (polymorphic many-to-many)

Define these relationships and write code to demonstrate eager loading, querying with relationship constraints, and creating related models.

E-commerce Relationships Exercise

Design the relationships for an e-commerce system with these requirements:

  1. Products belong to categories, which can be nested
  2. Products can have multiple variants (size, color, etc.)
  3. Users can create orders containing multiple products
  4. Orders have shipping and billing addresses
  5. Users can review products they've purchased
  6. Products can be tagged with multiple attributes
  7. Products can be added to users' wishlists

Create the model relationship definitions and write example code for common operations like finding a user's orders, a product's reviews, and listing products by category (including subcategories).

Summary

In the next lecture, we'll explore Eloquent querying and the database operations in more detail, building on our understanding of models and their relationships.