Eloquent ORM and Database Migrations

Building robust data layers and managing database schema in Laravel

Introduction to Eloquent ORM

Eloquent is Laravel's implementation of the Active Record pattern, providing an elegant, expressive ORM (Object-Relational Mapper) for working with your database. It allows you to interact with database tables using object-oriented syntax rather than raw SQL.

Think of Eloquent as a translator between two different worlds: your object-oriented PHP application and your relational database. Just as a skilled translator enables seamless communication between people speaking different languages, Eloquent enables your application to communicate with your database using natural, intuitive PHP code.

"Eloquent ORM is the perfect balance between simplicity and power, allowing you to build applications quickly without sacrificing capabilities when your data needs grow complex."
graph LR A[PHP Objects] <-->|Eloquent ORM| B[Database Records] style A fill:#7ef9a5 style B fill:#7ecbf9

Unlike query builders that focus purely on constructing SQL statements, Eloquent models represent specific database tables and individual records as objects, creating a more natural programming experience.

Creating and Using Models

Eloquent models are the heart of the ORM system. Each model corresponds to a database table, and each instance of a model represents a row in that table.

Generating a Model

// Basic model generation
php artisan make:model Product

// Model with migration
php artisan make:model Product -m

// Model with migration, factory, seeder, controller, and policy
php artisan make:model Product --all

Basic Model Structure

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    // Table name (if different from default)
    protected $table = 'products';
    
    // Primary key (if not 'id')
    protected $primaryKey = 'product_id';
    
    // Disable timestamps
    public $timestamps = false;
    
    // Mass assignable attributes
    protected $fillable = [
        'name', 'description', 'price', 'category_id'
    ];
    
    // Hidden attributes (not included in arrays/JSON)
    protected $hidden = [
        'wholesale_price'
    ];
    
    // Attribute casting
    protected $casts = [
        'price' => 'decimal:2',
        'active' => 'boolean',
        'options' => 'array',
        'released_at' => 'datetime'
    ];
}

Creating an Eloquent model is like creating a blueprint for a building. The blueprint defines the structure and characteristics of the building, just as the model defines the structure and behavior of your database table.

Working with Models

Once you've defined a model, you can use it to perform database operations:

CRUD Operations with Eloquent

// Create a new record
$product = new Product;
$product->name = 'Smartphone';
$product->price = 799.99;
$product->save();

// Alternative creation using mass assignment
$product = Product::create([
    'name' => 'Laptop',
    'price' => 1299.99,
    'category_id' => 2
]);

// Read (retrieve) records
$allProducts = Product::all();
$product = Product::find(1);
$expensiveProducts = Product::where('price', '>', 1000)->get();

// Update records
$product = Product::find(1);
$product->price = 899.99;
$product->save();

// Update using query
Product::where('category_id', 5)->update(['active' => true]);

// Delete records
$product = Product::find(1);
$product->delete();

// Delete using query
Product::destroy(1);
Product::destroy([1, 2, 3]);
Product::where('active', false)->delete();

Working with Eloquent models is similar to interacting with any physical object. You can create it, examine its properties, modify it, and eventually discard it - all using intuitive object-oriented syntax rather than thinking in terms of database tables and SQL.

Querying with Eloquent

Eloquent provides a powerful fluent interface for building database queries:

Basic Queries

// Retrieve all records
$products = Product::all();

// Retrieve by primary key
$product = Product::find(1);
$products = Product::find([1, 2, 3]);

// Find with exception if not found
$product = Product::findOrFail(1);

// Find by other columns
$product = Product::where('sku', 'ABC123')->first();

// Query builder methods
$products = Product::where('price', '>', 100)
                  ->where('category_id', 2)
                  ->orderBy('name')
                  ->limit(10)
                  ->get();

Advanced Queries

// Aggregates
$count = Product::where('active', true)->count();
$max = Product::where('category_id', 5)->max('price');
$avg = Product::avg('price');

// Chunking for large datasets
Product::chunk(100, function ($products) {
    foreach ($products as $product) {
        // Process each product
    }
});

// Using raw expressions
$products = Product::select('name')
                  ->selectRaw('price * ? as discounted_price', [0.9])
                  ->whereRaw('price > IF(discount_active, 100, 200)')
                  ->orderByRaw('FIELD(category, "featured", "standard", "sale")')
                  ->get();

// Joins
$usersWithPosts = User::join('posts', 'users.id', '=', 'posts.user_id')
                      ->select('users.*', 'posts.title')
                      ->get();

Using Scopes for Reusable Queries

// In the model
class Product extends Model
{
    // Global scope - applied to all queries
    protected static function booted()
    {
        static::addGlobalScope('active', function ($query) {
            $query->where('active', true);
        });
    }
    
    // Local scope - can be used when needed
    public function scopePopular($query)
    {
        return $query->where('views', '>', 1000);
    }
    
    public function scopePriceRange($query, $min, $max)
    {
        return $query->whereBetween('price', [$min, $max]);
    }
}

// Using local scopes
$popularProducts = Product::popular()->get();
$midRangeProducts = Product::priceRange(100, 500)->get();

// Removing global scopes
$allProducts = Product::withoutGlobalScope('active')->get();

Eloquent query building is like constructing a detailed search request at a library. Instead of telling the librarian "get me all books" (SELECT * FROM books), you can specify "get me science fiction books published after 2010, sorted by author" - all using a fluid, intuitive syntax.

Relationships

One of Eloquent's most powerful features is its elegant handling of database relationships. These allow you to define how your models are connected to each other, mirroring the relationships in your database schema.

classDiagram User "1" -- "*" Post : creates Post "*" -- "*" Tag : has Post "1" -- "*" Comment : contains User "1" -- "*" Comment : writes class User { +string name +string email +posts() +comments() } class Post { +string title +text content +user() +comments() +tags() } class Comment { +text body +user() +post() } class Tag { +string name +posts() }

Defining Relationships

// One to One
class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
        // Option: return $this->hasOne(Profile::class, 'foreign_key', 'local_key');
    }
}

class Profile extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
        // Option: return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
    }
}

// One to Many
class Post extends Model
{
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

class Comment extends Model
{
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

// Many to Many
class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
        // Options: return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id')
        //  - Arg 2: pivot table name
        //  - Arg 3: foreign key of the model on which you're defining the relationship
        //  - Arg 4: foreign key of the model that you're joining to
    }
}

class Role extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

// Has Many Through
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,     // Final model we want to access
            User::class      // Intermediate model
        );
    }
}

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

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

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

Using Relationships

// Eager loading (avoiding N+1 query problem)
$posts = Post::with('user', 'comments', 'tags')->get();

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

// Constraining eager loads
$users = User::with(['posts' => function ($query) {
    $query->where('active', true);
}])->get();

// Counting related models
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

// Eager loading specific columns
$posts = Post::with('user:id,name')->get();

// Inserting related models
$comment = new Comment(['message' => 'A new comment']);
$post = Post::find(1);
$post->comments()->save($comment);

// Alternative using create
$post->comments()->create([
    'message' => 'Another comment'
]);

// Many to Many operations
$user = User::find(1);
$user->roles()->attach(1); // Attach role with ID 1
$user->roles()->detach(1); // Detach role with ID 1
$user->roles()->sync([1, 2, 3]); // Sync with only these roles
$user->roles()->toggle([1, 3]); // Attach if not exists, detach if exists

Eloquent relationships mirror real-world connections between entities. Just as a person has many friends, a post has many comments. And just as you can ask a person about their friends, you can ask a post model about its comments - Eloquent handles the complex SQL needed behind the scenes.

Accessors and Mutators

Accessors and mutators allow you to transform attribute values when retrieving or setting them, keeping your data handling consistent and encapsulated within your models.

Defining Accessors and Mutators

class User extends Model
{
    // Accessor (get)
    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }
    
    // Mutator (set)
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }
}

// Usage
$user = User::find(1);
echo $user->full_name; // Uses the accessor

$user = new User;
$user->password = 'plain-text'; // Uses the mutator, stores encrypted
$user->save();

Attribute Casting

class Product extends Model
{
    protected $casts = [
        'price' => 'decimal:2',
        'active' => 'boolean',
        'options' => 'array',
        'metadata' => 'object',
        'settings' => 'collection',
        'released_at' => 'datetime',
        'dimensions' => 'json',
    ];
}

Accessors and mutators are like automatic converters in your model. Imagine a currency converter that automatically converts dollars to euros - accessors and mutators similarly transform data between your database format and your application format, ensuring consistency.

Database Migrations Overview

Migrations are Laravel's way of handling database schema changes over time, essentially serving as version control for your database. They allow you to modify your database structure in a consistent, repeatable way across all environments.

graph LR A[Development DB] -->|migrate| B(Schema V1) B -->|migrate| C(Schema V2) C -->|migrate| D(Schema V3) D -->|rollback| C A -.->|Export changes as migration| E[Version Control] E -.->|Apply migrations| F[Production DB] style A fill:#7ecbf9 style E fill:#f9d77e style F fill:#7ecbf9

Think of migrations as a time machine for your database. They allow you to move forward (migrate) to apply schema changes or backward (rollback) to undo them, ensuring your database structure evolves alongside your application code.

Creating Migrations

// Basic migration
php artisan make:migration create_products_table

// Migration for specific table
php artisan make:migration add_category_id_to_products --table=products

// Migration with model
php artisan make:model Product -m

Migration Structure

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 8, 2);
            $table->foreignId('category_id')->constrained();
            $table->boolean('active')->default(true);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
}

Writing Effective Migrations

Migrations use Laravel's Schema Builder to define database changes in a database-agnostic way, allowing your application to work with various database systems without changing code.

Common Column Types

Schema::create('example', function (Blueprint $table) {
    // Basic types
    $table->id(); // Auto-incrementing BIGINT
    $table->bigIncrements('id'); // Auto-incrementing BIGINT
    $table->increments('id'); // Auto-incrementing INT
    
    // String types
    $table->string('name', 100); // VARCHAR with length
    $table->text('description'); // TEXT
    $table->mediumText('content'); // MEDIUMTEXT
    $table->longText('article'); // LONGTEXT
    
    // Number types
    $table->integer('count');
    $table->bigInteger('big_count');
    $table->float('amount', 8, 2); // 8 total digits, 2 decimal places
    $table->decimal('price', 8, 2); // 8 total digits, 2 decimal places
    
    // Boolean
    $table->boolean('active');
    
    // Date and time
    $table->date('birth_date');
    $table->time('opening_time');
    $table->dateTime('published_at');
    $table->timestamp('logged_at');
    $table->timestamps(); // created_at & updated_at
    $table->softDeletes(); // deleted_at for soft deletes
    
    // Other types
    $table->binary('data');
    $table->uuid('id');
    $table->json('options');
    $table->jsonb('settings'); // PostgreSQL JSONB
    $table->enum('status', ['pending', 'active', 'completed']);
});

Column Modifiers

Schema::create('example', function (Blueprint $table) {
    $table->string('email')->unique(); // Unique index
    $table->string('slug')->index(); // Regular index
    $table->text('description')->nullable(); // Allow NULL
    $table->timestamp('created_at')->useCurrent(); // Default to CURRENT_TIMESTAMP
    $table->integer('score')->default(0); // Default value
    $table->string('uuid')->primary(); // Make this the primary key
    $table->foreignId('user_id')->unsigned(); // Unsigned (positive only)
    $table->text('notes')->fulltext(); // Full-text index
    $table->integer('priority')->comment('Task priority level'); // Database comment
});

Foreign Keys & Relationships

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->foreignId('user_id')->constrained(); // Creates constraint automatically
    $table->timestamps();
});

// More control over foreign keys
Schema::table('comments', function (Blueprint $table) {
    // Basic foreign key
    $table->foreignId('post_id')
          ->constrained();
          
    // Expanded version with more control
    $table->unsignedBigInteger('post_id');
    $table->foreign('post_id')
          ->references('id')
          ->on('posts')
          ->onDelete('cascade')
          ->onUpdate('cascade');
});

Modifying Tables

Schema::table('users', function (Blueprint $table) {
    // Add new columns
    $table->string('middle_name')->after('first_name')->nullable();
    
    // Change column type (requires doctrine/dbal package)
    $table->string('name', 50)->change(); // Change length
    $table->text('description')->nullable(false)->change(); // Remove nullable
    
    // Rename column
    $table->renameColumn('email', 'email_address');
    
    // Drop columns
    $table->dropColumn('votes');
    $table->dropColumn(['votes', 'avatar', 'location']);
});

// Renaming tables
Schema::rename('old_name', 'new_name');

// Dropping tables
Schema::drop('users');
Schema::dropIfExists('users');

Writing migrations is similar to planning a building renovation. You create detailed blueprints (migrations) showing what to add, modify, or remove, and then execute those plans in a controlled sequence to transform the building (database) without disrupting its inhabitants (your data).

Migration Best Practices

Following these practices will help you maintain a healthy, manageable migration system:

Keep Migrations Focused

  • Each migration should have a single responsibility
  • Name migrations descriptively (e.g., create_users_table, add_api_token_to_users)
  • Break complex schema changes into multiple migrations

Plan for Reversibility

  • Always implement the down() method properly
  • Test both migrating and rolling back
  • Be careful with migrations that destroy data

Use Foreign Key Constraints

  • Let the database help maintain referential integrity
  • Use constrained() to follow Laravel naming conventions
  • Consider impact of constraints on seeding and testing

Think About Deployment

  • Write migrations that can run safely in production
  • Be cautious with migrations that lock tables for long periods
  • Use ->after() to position columns efficiently
  • Consider breaking large table alterations into smaller steps

Advanced Migration Techniques

// Safely adding a unique constraint to existing data
Schema::table('users', function (Blueprint $table) {
    // First add the column without the constraint
    $table->string('username')->nullable();
});

// Here you would run code to populate the column with unique values

Schema::table('users', function (Blueprint $table) {
    // Then add the unique constraint and remove nullable
    $table->string('username')->nullable(false)->unique()->change();
});

// Using raw SQL for complex operations
Schema::table('large_table', function (Blueprint $table) {
    DB::statement('CREATE INDEX CONCURRENTLY idx_large_table_complex ON large_table (complex_expression)');
});

Migration best practices are like architectural principles for evolving buildings. They ensure changes are well-documented, reversible when needed, structurally sound, and can be implemented with minimal disruption to the building's users.

Seeding Database with Test Data

Database seeding provides a way to populate your database with test or default data. This is especially useful for development, testing, and having a consistent starting point for new installations.

Creating and Running Seeders

// Generate a seeder
php artisan make:seeder UserSeeder

// Run all seeders
php artisan db:seed

// Run a specific seeder
php artisan db:seed --class=UserSeeder

// Fresh migrations with seeding
php artisan migrate:fresh --seed

Basic Seeder Structure

use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UserSeeder extends Seeder
{
    public function run()
    {
        // Create admin user
        User::create([
            'name' => 'Admin User',
            'email' => 'admin@example.com',
            'password' => Hash::make('password'),
            'role' => 'admin'
        ]);
        
        // Create multiple regular users
        for ($i = 1; $i <= 10; $i++) {
            User::create([
                'name' => "User $i",
                'email' => "user$i@example.com",
                'password' => Hash::make('password'),
                'role' => 'user'
            ]);
        }
    }
}

Using Factories with Seeders

use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Post;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // Call other seeders
        $this->call([
            RoleSeeder::class,
            PermissionSeeder::class,
        ]);
        
        // Create users and their posts
        User::factory(10)
            ->has(Post::factory()->count(3))
            ->create();
            
        // Create specific user with posts
        User::factory()
            ->state([
                'name' => 'John Doe',
                'email' => 'john@example.com',
            ])
            ->has(Post::factory()->count(5))
            ->create();
    }
}

Model Factory Example

// Generate a factory
php artisan make:factory PostFactory --model=Post

// Factory definition
namespace Database\Factories;

use App\Models\Post;
use App\Models\User;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition()
    {
        return [
            'title' => $this->faker->sentence(),
            'content' => $this->faker->paragraphs(3, true),
            'published' => $this->faker->boolean(80),
            'user_id' => User::factory(),
            'category_id' => Category::inRandomOrder()->first()->id ?? Category::factory(),
            'published_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
        ];
    }
    
    // Factory states for different scenarios
    public function featured()
    {
        return $this->state(function (array $attributes) {
            return [
                'featured' => true,
                'featured_image' => $this->faker->imageUrl(),
            ];
        });
    }
    
    public function draft()
    {
        return $this->state([
            'published' => false,
            'published_at' => null,
        ]);
    }
}

Database seeding is like setting up a model home before showing properties to potential buyers. It ensures the database has realistic, well-structured data that showcases the application's features and provides a consistent environment for development and testing.

Practical Example: Building a Blog Database

Let's put these concepts together with a practical example of building a complete blog database structure with Eloquent models, migrations, and relationships:

erDiagram USERS ||--o{ POSTS : writes USERS ||--o{ COMMENTS : writes POSTS ||--o{ COMMENTS : has POSTS }o--|| CATEGORIES : belongs-to POSTS }o--o{ TAGS : has USERS { id int PK name string email string password string created_at datetime updated_at datetime } POSTS { id int PK title string slug string content text user_id int FK category_id int FK published boolean published_at datetime created_at datetime updated_at datetime } COMMENTS { id int PK post_id int FK user_id int FK content text approved boolean created_at datetime updated_at datetime } CATEGORIES { id int PK name string slug string description text created_at datetime updated_at datetime } TAGS { id int PK name string slug string created_at datetime updated_at datetime } POST_TAG { post_id int FK tag_id int FK }

Migrations

// create_categories_table migration
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->timestamps();
});

// create_posts_table migration
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('content');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->foreignId('category_id')->constrained();
    $table->boolean('published')->default(false);
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
});

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

// create_post_tag_table migration (pivot)
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']);
});

// create_comments_table migration
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->onDelete('cascade');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->text('content');
    $table->boolean('approved')->default(false);
    $table->timestamps();
});

Model Definitions

// User.php (extending existing model)
class User extends Authenticatable
{
    // Existing user model code...
    
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
    
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

// Post.php
class Post extends Model
{
    use HasFactory;
    
    protected $fillable = [
        'title', 'slug', 'content', 'category_id', 
        'published', 'published_at'
    ];
    
    protected $casts = [
        'published' => 'boolean',
        'published_at' => 'datetime',
    ];
    
    // Relations
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
    
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
    
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
    
    // Scopes
    public function scopePublished($query)
    {
        return $query->where('published', true)
                     ->where('published_at', '<=', now());
    }
    
    public function scopeLatest($query)
    {
        return $query->orderBy('published_at', 'desc');
    }
    
    public function scopeWithCategory($query, $category)
    {
        return $query->whereHas('category', function ($q) use ($category) {
            $q->where('slug', $category);
        });
    }
    
    public function scopeWithTag($query, $tag)
    {
        return $query->whereHas('tags', function ($q) use ($tag) {
            $q->where('slug', $tag);
        });
    }
    
    // Accessors & Mutators
    public function getExcerptAttribute()
    {
        return Str::limit(strip_tags($this->content), 150);
    }
    
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = $value;
        $this->attributes['slug'] = Str::slug($value);
    }
}

// Category.php, Tag.php, Comment.php models follow similar patterns

Using the Blog Models

// Creating a new post with relationships
$post = new Post([
    'title' => 'My First Blog Post',
    'content' => '# Introduction...',
    'category_id' => $category->id,
    'published' => true,
    'published_at' => now(),
]);

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

// Attach tags
$post->tags()->attach([
    Tag::firstOrCreate(['name' => 'Laravel', 'slug' => 'laravel'])->id,
    Tag::firstOrCreate(['name' => 'Eloquent', 'slug' => 'eloquent'])->id,
]);

// Querying the blog
$recentPosts = Post::with('user', 'category', 'tags')
                   ->published()
                   ->latest()
                   ->paginate(10);
                   
$categoryPosts = Post::with('user', 'tags')
                     ->published()
                     ->withCategory('tutorials')
                     ->latest()
                     ->paginate(10);
                     
$taggedPosts = Post::published()
                   ->withTag('eloquent')
                   ->withCount('comments')
                   ->latest()
                   ->get();

This blog example demonstrates how Eloquent and migrations work together to create a complete, well-structured database layer for your application. The models encapsulate both data structure and business logic, while migrations provide a reproducible way to set up the database schema.

Practical Activity: E-commerce Product Catalog

Let's solidify your understanding with a hands-on activity:

Activity: Build a Product Catalog System

  1. Create the following migrations and models:
    • Categories (with parent-child relationship)
    • Products (belonging to categories)
    • Product Attributes (e.g., color, size)
    • Product Variants (combinations of attributes)
  2. Implement the migrations with proper column types, constraints, and indexes
  3. Define Eloquent relationships between models
  4. Create model methods for:
    • Getting product price range (min/max across variants)
    • Checking if a product is in stock
    • Finding products by attribute values
  5. Create database seeders with factories to generate test products
  6. Write example code to:
    • Create a product with multiple variants
    • Query products by category, attributes, and price range
    • Implement a breadcrumb trail for nested categories

Extension: Add product reviews with a polymorphic relationship that could also be used for categories.

Advanced Eloquent Techniques

As your application grows, you'll benefit from these advanced Eloquent features:

Events and Observers

Eloquent models dispatch events when actions occur, allowing you to hook into the model lifecycle:

// In your model
protected static function booted()
{
    static::created(function ($model) {
        // Do something after model is created
    });
}

// Or using an observer
php artisan make:observer ProductObserver --model=Product

class ProductObserver
{
    public function created(Product $product)
    {
        // Handle the event
    }
    
    public function updated(Product $product)
    {
        // Handle the event
    }
    
    // Other events: creating, saving, saved, 
    // deleting, deleted, restoring, restored
}

Soft Deletes

Keep records in the database but mark them as deleted:

// In migration
$table->softDeletes();

// In model
use SoftDeletes;

// Usage
$product->delete(); // Soft delete
$product->forceDelete(); // Real delete

// Querying
Product::withTrashed()->get(); // All records
Product::onlyTrashed()->get(); // Only deleted
$product->trashed(); // Check if deleted

// Restoring
$product->restore();

Query Scopes

Encapsulate common query constraints:

// Local scope
public function scopeActive($query)
{
    return $query->where('active', true);
}

// Global scope
protected static function booted()
{
    static::addGlobalScope('ancient', function ($query) {
        $query->where('created_at', '<', 
            now()->subYears(2000));
    });
}

// Class-based global scope
class AncientScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('created_at', '<', 
            now()->subYears(2000));
    }
}

Eager Loading Nested Relationships

Load deeply nested relationships efficiently:

// Nested eager loading
$users = User::with('posts.comments.author')
             ->get();

// Lazy eager loading
$user->load('posts.comments');

// Nested eager loading constraints
$users = User::with(['posts' => function ($query) {
    $query->where('active', true);
    
    $query->with(['comments' => function ($query) {
        $query->where('approved', true);
    }]);
}])->get();

Subqueries in Eloquent

Use subqueries for complex operations:

// Select users with their latest post title
$users = User::select(['users.*', 'posts.title as latest_post_title'])
    ->leftJoinSub(
        Post::select('user_id', 'title')
            ->whereIn('id', function ($query) {
                $query->select(DB::raw('MAX(id)'))
                    ->from('posts')
                    ->groupBy('user_id');
            }),
        'posts',
        'users.id',
        '=',
        'posts.user_id'
    )->get();

Using Raw Expressions

Combine Eloquent with raw SQL when needed:

$users = DB::table('users')
    ->select(DB::raw('COUNT(*) as user_count, status'))
    ->where('status', '<>', 1)
    ->groupBy('status')
    ->get();
    
Product::where('price', '>', function ($query) {
    $query->selectRaw('AVG(price) * 1.5')
          ->from('products');
})->get();

These advanced techniques are like specialized tools in a craftsman's toolbox - they might not be needed for every job, but when you encounter specific challenges, they provide elegant solutions that maintain the clean, expressive nature of Eloquent.

Summary and Key Takeaways

With these tools, Laravel provides a complete solution for database interaction that balances power and flexibility with elegant, expressive syntax - making database operations a joy rather than a chore.

Further Resources