05: Working with Data: Eloquent ORM & Database Workflow

05: Working with Data: Eloquent ORM & Database Workflow Intermediate
Section titled “05: Working with Data: Eloquent ORM & Database Workflow Intermediate”Overview
Section titled “Overview”If you’ve worked with ActiveRecord, you already understand the Active Record pattern: models that map to database tables, relationships that connect data, migrations that version your schema, and query builders that let you chain methods. Eloquent ORM-Laravel’s database abstraction layer-follows the exact same principles with PHP syntax.
This chapter shows you ActiveRecord code you know, then demonstrates the Eloquent equivalent. You’ll discover that working with databases in Laravel feels remarkably similar to Rails.
Prerequisites
Section titled “Prerequisites”Before starting this chapter, you should have:
- Completion of Chapter 04: PHP Syntax & Language Differences for Rails Devs or equivalent understanding
- Familiarity with ActiveRecord in Rails (models, relationships, migrations, queries)
- Basic understanding of database concepts (tables, relationships, indexes)
- PHP 8.4+ installed (optional for this chapter, but recommended)
- Estimated Time: ~60-75 minutes
Verify your setup (optional):
# Check PHP version if you have it installedphp --versionWhat You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will understand:
- How to define Eloquent models with relationships (equivalent to ActiveRecord models)
- How to query databases using Eloquent (equivalent to ActiveRecord queries)
- How to use scopes, eager loading, and model events
- How to write migrations and seeders
- How to work with factories for testing
- How to implement advanced patterns like soft deletes and transactions
You’ll be able to translate any ActiveRecord pattern you know into its Eloquent equivalent.
What You’ll Learn
Section titled “What You’ll Learn”- Model definitions and conventions
- Eloquent relationships vs ActiveRecord associations
- Dependent options and cascading deletes
- Query building and scopes
- Global scopes (default_scope equivalent)
- Eager loading (N+1 prevention)
- Touch associations
- Migrations and schema building
- Model events (like ActiveRecord callbacks)
- Accessors, mutators, and casts
- Enum attributes and casting
- Model validation (with Laravel’s preferred approach)
- Database transactions
- Advanced patterns
📦 Code Samples
Section titled “📦 Code Samples”Eloquent ORM patterns and examples:
- Eloquent ORM Examples — Query patterns, scopes, relationships, and best practices
Production-ready project with complete Eloquent usage:
- TaskMaster Application — Full Laravel app with models, migrations, relationships, factories, and tests
Access all code samples:
git clone https://github.com/dalehurley/codewithphp.gitcd codewithphp/code/rails-developers-love-laravelQuick ORM Comparison
Section titled “Quick ORM Comparison”| Rails (ActiveRecord) | Laravel (Eloquent) |
|---|---|
rails g model User | artisan make:model User -m |
belongs_to :user | belongsTo(User::class) |
has_many :posts | hasMany(Post::class) |
has_and_belongs_to_many | belongsToMany() |
scope :active | scopeActive($query) |
User.where(...) | User::where(...)->get() |
includes(:posts) | with('posts') |
before_save :callback | static::saving(fn($model) => ...) |
validates :name | Form Request validation |
.pluck(:name) | ->pluck('name') |
1. Model Basics
Section titled “1. Model Basics”Rails Model
Section titled “Rails Model”class User < ApplicationRecord # Table name automatically inferred: users # Primary key automatically: id # Timestamps automatically: created_at, updated_atend
# Usageuser = User.new(name: 'John', email: 'john@example.com')user.save
user = User.create(name: 'John', email: 'john@example.com')user = User.find(1)user.update(name: 'Jane')user.destroyLaravel Model
Section titled “Laravel Model”<?phpnamespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model{ // Table name automatically inferred: users // Primary key automatically: id // Timestamps automatically: created_at, updated_at
// Mass assignment protection protected $fillable = ['name', 'email']; // Or specify guarded (opposite) // protected $guarded = [];}
// Usage$user = new User(['name' => 'John', 'email' => 'john@example.com']);$user->save();
$user = User::create(['name' => 'John', 'email' => 'john@example.com']);$user = User::find(1);$user->update(['name' => 'Jane']);$user->delete();::: tip Mass Assignment Protection
Laravel requires you to explicitly define $fillable (whitelist) or $guarded (blacklist) for mass assignment. Rails uses strong parameters in controllers instead.
:::
Custom Table Names and Keys
Section titled “Custom Table Names and Keys”Rails:
class User < ApplicationRecord self.table_name = 'custom_users' self.primary_key = 'user_id'endLaravel:
<?phpclass User extends Model{ protected $table = 'custom_users'; protected $primaryKey = 'user_id'; public $incrementing = false; // If not auto-increment protected $keyType = 'string'; // If UUID}2. Relationships
Section titled “2. Relationships”Eloquent relationships are nearly identical to ActiveRecord associations.
One-to-Many
Section titled “One-to-Many”Rails:
class User < ApplicationRecord has_many :posts, dependent: :destroyend
# app/models/post.rbclass Post < ApplicationRecord belongs_to :userend
# Usageuser = User.firstuser.posts # => [Post, Post, ...]user.posts.create(title: 'Hello')
post = Post.firstpost.user # => UserLaravel:
<?phpclass User extends Model{ public function posts() { return $this->hasMany(Post::class); }}
// app/Models/Post.phpclass Post extends Model{ public function user() { return $this->belongsTo(User::class); }}
// Usage$user = User::first();$user->posts; // => Collection of Post models$user->posts()->create(['title' => 'Hello']);
$post = Post::first();$post->user; // => User::: tip Dependent Options and Cascades
In Rails, you use dependent: :destroy on the relationship. In Laravel, cascading deletes are typically handled at the database level in migrations using cascadeOnDelete():
// Migration$table->foreignId('user_id') ->constrained() ->cascadeOnDelete(); // Equivalent to dependent: :destroyFor model-level cleanup (like dependent: :delete_all or dependent: :nullify), use model events in Laravel:
// In User modelprotected static function booted(){ static::deleting(function ($user) { // Option 1: Delete all posts (delete_all) $user->posts()->delete();
// Option 2: Nullify foreign keys (nullify) // $user->posts()->update(['user_id' => null]); });}:::
One-to-One
Section titled “One-to-One”Rails:
class User < ApplicationRecord has_one :profileend
class Profile < ApplicationRecord belongs_to :userendLaravel:
<?phpclass User extends Model{ public function profile() { return $this->hasOne(Profile::class); }}
class Profile extends Model{ public function user() { return $this->belongsTo(User::class); }}Many-to-Many
Section titled “Many-to-Many”Rails:
class Post < ApplicationRecord has_and_belongs_to_many :tags # Or with through: # has_many :post_tags # has_many :tags, through: :post_tagsend
class Tag < ApplicationRecord has_and_belongs_to_many :postsend
# Usagepost.tags << Tag.firstpost.tags.attach(tag_id)Laravel:
<?phpclass Post extends Model{ public function tags() { return $this->belongsToMany(Tag::class); }}
class Tag extends Model{ public function posts() { return $this->belongsToMany(Post::class); }}
// Usage$post->tags()->attach($tagId);$post->tags()->detach($tagId);$post->tags()->sync([1, 2, 3]);Pivot Table Data
Section titled “Pivot Table Data”Rails:
class Post < ApplicationRecord has_many :post_tags has_many :tags, through: :post_tagsend
class PostTag < ApplicationRecord belongs_to :post belongs_to :tagend
# Access pivot datapost.post_tags.first.created_atLaravel:
<?phpclass Post extends Model{ public function tags() { return $this->belongsToMany(Tag::class) ->withPivot('created_at', 'updated_at') ->withTimestamps(); }}
// Access pivot data$post->tags->first()->pivot->created_at;Polymorphic Relationships
Section titled “Polymorphic Relationships”Rails:
class Comment < ApplicationRecord belongs_to :commentable, polymorphic: trueend
class Post < ApplicationRecord has_many :comments, as: :commentableend
class Video < ApplicationRecord has_many :comments, as: :commentableendLaravel:
<?phpclass 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'); }}Touch Associations
Section titled “Touch Associations”Rails:
class Post < ApplicationRecord belongs_to :user, touch: true # Updates user.updated_at when post is savedendLaravel:
<?phpclass Post extends Model{ public function user() { return $this->belongsTo(User::class)->withDefault(); }
// Manual touch in model events protected static function booted() { static::saved(function ($post) { $post->user->touch(); // Update user's updated_at }); }}
// Or use $touches property (shorthand)class Post extends Model{ protected $touches = ['user']; // Auto-touches user when post is saved}::: tip Touch on Update
Both frameworks allow you to automatically update a parent model’s updated_at timestamp when a child model is saved. Laravel’s $touches property is cleaner than manual events.
:::
3. Querying
Section titled “3. Querying”Basic Queries
Section titled “Basic Queries”Rails:
# Find by IDuser = User.find(1)user = User.find_by(email: 'john@example.com')
# Where clausesusers = User.where(active: true)users = User.where('age > ?', 18)users = User.where(active: true).where('age > ?', 18)
# Orderingusers = User.order(created_at: :desc)users = User.order('name ASC, created_at DESC')
# Limitingusers = User.limit(10)users = User.limit(10).offset(20)
# Selecting specific columnsusers = User.select(:id, :name, :email)
# First, last, alluser = User.firstuser = User.lastusers = User.allLaravel:
<?php// Find by ID$user = User::find(1);$user = User::where('email', 'john@example.com')->first();// Or shorthand$user = User::firstWhere('email', 'john@example.com');
// Where clauses$users = User::where('active', true)->get();$users = User::where('age', '>', 18)->get();$users = User::where('active', true)->where('age', '>', 18)->get();
// Ordering$users = User::orderBy('created_at', 'desc')->get();$users = User::orderBy('name')->orderBy('created_at', 'desc')->get();
// Limiting$users = User::limit(10)->get();$users = User::skip(20)->take(10)->get();
// Selecting specific columns$users = User::select('id', 'name', 'email')->get();
// First, all$user = User::first();$user = User::latest()->first(); // Latest by created_at$users = User::all();::: tip Explicit .get()
Unlike Rails where queries execute automatically, Laravel requires explicit ->get(), ->first(), or ->all() to execute queries. This makes it clearer when database calls happen.
:::
Advanced Queries
Section titled “Advanced Queries”Rails:
# OR conditionsusers = User.where(role: 'admin').or(User.where(role: 'moderator'))
# IN queriesusers = User.where(id: [1, 2, 3])
# LIKE queriesusers = User.where('name LIKE ?', '%john%')
# Joinsusers = User.joins(:posts).where(posts: { published: true })
# Count, sum, averagecount = User.counttotal = Order.sum(:amount)avg = Order.average(:amount)
# Distinctusers = User.select(:email).distinct
# Existsexists = User.where(email: 'test@example.com').exists?
# Raw SQLusers = User.find_by_sql('SELECT * FROM users WHERE ...')Laravel:
<?php// OR conditions$users = User::where('role', 'admin') ->orWhere('role', 'moderator') ->get();
// IN queries$users = User::whereIn('id', [1, 2, 3])->get();
// LIKE queries$users = User::where('name', 'LIKE', '%john%')->get();
// Joins$users = User::join('posts', 'users.id', '=', 'posts.user_id') ->where('posts.published', true) ->get();
// Count, sum, average$count = User::count();$total = Order::sum('amount');$avg = Order::avg('amount');
// Distinct$users = User::select('email')->distinct()->get();
// Exists$exists = User::where('email', 'test@example.com')->exists();
// Raw SQL$users = DB::select('SELECT * FROM users WHERE ...');4. Scopes
Section titled “4. Scopes”Scopes let you reuse query logic, just like Rails.
Rails:
class Post < ApplicationRecord scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } scope :by_author, ->(author_id) { where(author_id: author_id) }
# Can also use class methods def self.featured where(featured: true).order(views: :desc) endend
# UsagePost.publishedPost.recentPost.by_author(user.id)Post.published.recentLaravel:
<?phpclass Post extends Model{ public function scopePublished($query) { return $query->where('published', true); }
public function scopeRecent($query) { return $query->orderBy('created_at', 'desc')->limit(10); }
public function scopeByAuthor($query, $authorId) { return $query->where('author_id', $authorId); }
// Can also use static methods public static function featured() { return static::where('featured', true)->orderBy('views', 'desc')->get(); }}
// Usage (Laravel automatically removes 'scope' prefix)Post::published()->get();Post::recent()->get();Post::byAuthor($user->id)->get();Post::published()->recent()->get();Global Scopes (Default Scopes)
Section titled “Global Scopes (Default Scopes)”Rails:
class Post < ApplicationRecord default_scope { where(published: true) } # Or unscoped to bypassend
# UsagePost.all # Automatically includes where(published: true)Laravel:
<?phpuse Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model;
class Post extends Model{ protected static function booted() { static::addGlobalScope('published', function (Builder $builder) { $builder->where('published', true); }); }}
// UsagePost::all(); // Automatically includes where('published', true)
// Bypass global scopePost::withoutGlobalScope('published')->get();Post::withoutGlobalScopes()->get(); // Remove all global scopes::: tip Global Scopes
Both Rails default_scope and Laravel global scopes apply automatically to all queries. Use sparingly and document them well, as they can make debugging harder when queries behave unexpectedly.
:::
5. Eager Loading (N+1 Prevention)
Section titled “5. Eager Loading (N+1 Prevention)”Both frameworks provide eager loading to prevent N+1 queries.
Rails:
# N+1 problemposts = Post.allposts.each do |post| puts post.user.name # Query for each post!end
# Solution: eager loadingposts = Post.includes(:user)posts.each do |post| puts post.user.name # No additional queries!end
# Multiple relationsposts = Post.includes(:user, :comments, :tags)
# Nested relationsposts = Post.includes(comments: :user)Laravel:
<?php// N+1 problem$posts = Post::all();foreach ($posts as $post) { echo $post->user->name; // Query for each post!}
// Solution: eager loading$posts = Post::with('user')->get();foreach ($posts as $post) { echo $post->user->name; // No additional queries!}
// Multiple relations$posts = Post::with(['user', 'comments', 'tags'])->get();
// Nested relations$posts = Post::with('comments.user')->get();
// Conditional eager loading$posts = Post::with(['comments' => function ($query) { $query->where('approved', true);}])->get();6. Model Events (Callbacks)
Section titled “6. Model Events (Callbacks)”Rails:
class Post < ApplicationRecord before_save :generate_slug after_create :notify_followers before_destroy :cleanup_files
private
def generate_slug self.slug = title.parameterize end
def notify_followers # Send notifications end
def cleanup_files # Delete associated files endendLaravel:
<?phpclass Post extends Model{ protected static function booted() { static::saving(function ($post) { $post->slug = \Str::slug($post->title); });
static::created(function ($post) { // Send notifications });
static::deleting(function ($post) { // Cleanup files }); }}
// Or use observers for complex logic// php artisan make:observer PostObserver --model=PostAvailable events:
creating,createdupdating,updatedsaving,saved(fires on both create and update)deleting,deletedrestoring,restored(soft deletes)
7. Accessors and Mutators
Section titled “7. Accessors and Mutators”Rails:
class User < ApplicationRecord def full_name "#{first_name} #{last_name}" end
def full_name=(value) parts = value.split(' ') self.first_name = parts[0] self.last_name = parts[1] endend
# Usageuser.full_name = 'John Doe'puts user.full_name # => "John Doe"Laravel (Traditional):
<?phpclass User extends Model{ // Accessor (get) public function getFullNameAttribute() { return "{$this->first_name} {$this->last_name}"; }
// Mutator (set) public function setFullNameAttribute($value) { $parts = explode(' ', $value); $this->attributes['first_name'] = $parts[0]; $this->attributes['last_name'] = $parts[1] ?? ''; }}
// Usage$user->full_name = 'John Doe';echo $user->full_name; // => "John Doe"Laravel 8.4+ (Property Hooks):
<?phpclass User extends Model{ public string $full_name { get => "{$this->first_name} {$this->last_name}";
set(string $value) { [$this->first_name, $this->last_name] = explode(' ', $value); } }}8. Attribute Casting
Section titled “8. Attribute Casting”Rails:
class Post < ApplicationRecord # Rails infers types from database schema # For custom casting: attribute :metadata, :json attribute :published_at, :datetimeendLaravel:
<?phpclass Post extends Model{ protected $casts = [ 'published_at' => 'datetime', 'metadata' => 'array', 'is_published' => 'boolean', 'views' => 'integer', 'rating' => 'decimal:2', ];}
// Usage$post->metadata = ['key' => 'value']; // Automatically JSON-encoded$post->save();
$data = $post->metadata; // Automatically decoded to arrayRails:
class Post < ApplicationRecord enum status: { draft: 0, published: 1, archived: 2 }end
# Usagepost.status = 'published'post.published? # => truePost.published # => [Post, Post, ...]Laravel (8.75+):
<?phpuse App\Enums\PostStatus;
class Post extends Model{ protected $casts = [ 'status' => PostStatus::class, ];}
// Define enum class (PHP 8.1+)enum PostStatus: string{ case DRAFT = 'draft'; case PUBLISHED = 'published'; case ARCHIVED = 'archived';}
// Usage$post->status = PostStatus::PUBLISHED;$post->status === PostStatus::PUBLISHED; // => truePost::where('status', PostStatus::PUBLISHED)->get();::: tip Enums in Laravel Laravel 8.75+ introduced native enum support. Before that, you’d use string/integer values with constants or a package. PHP 8.1+ native enums are type-safe and work seamlessly with Eloquent. :::
Model Validation
Section titled “Model Validation”Rails:
class Post < ApplicationRecord validates :title, presence: true, length: { minimum: 5 } validates :slug, uniqueness: true validates :published_at, presence: true, if: :published?endLaravel:
<?phpuse Illuminate\Validation\ValidationException;
class Post extends Model{ protected static function booted() { static::saving(function ($post) { $validator = \Validator::make($post->toArray(), [ 'title' => 'required|min:5', 'slug' => 'unique:posts,slug,' . $post->id, 'published_at' => 'required_if:published,true', ]);
if ($validator->fails()) { throw ValidationException::withMessages($validator->errors()->toArray()); } }); }}::: warning Laravel Validation Philosophy While Laravel can validate at the model level (like Rails), Laravel’s preferred approach is Form Request validation at the controller level. This separates validation logic from models and makes it reusable. Model-level validation is useful for data integrity, but most validation should happen in Form Request classes. See Chapter 06: Building REST APIs for Form Request examples. :::
9. Soft Deletes
Section titled “9. Soft Deletes”Rails:
# Requires paranoia gem or custom implementationgem 'paranoia'
class Post < ApplicationRecord acts_as_paranoidend
post.destroy # Soft deletepost.really_destroy! # Hard deletePost.with_deleted # Include soft-deletedLaravel:
<?phpuse Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model{ use SoftDeletes;}
// Migration must have deleted_at column// $table->softDeletes();
$post->delete(); // Soft delete$post->forceDelete(); // Hard deletePost::withTrashed()->get(); // Include soft-deletedPost::onlyTrashed()->get(); // Only soft-deleted$post->restore(); // Restore soft-deleted10. Transactions
Section titled “10. Transactions”Rails:
ActiveRecord::Base.transaction do user.save! post.save! # All or nothingendLaravel:
<?phpuse Illuminate\Support\Facades\DB;
DB::transaction(function () use ($user, $post) { $user->save(); $post->save(); // All or nothing});
// Or manual controlDB::beginTransaction();try { $user->save(); $post->save(); DB::commit();} catch (\Exception $e) { DB::rollBack(); throw $e;}11. Pagination
Section titled “11. Pagination”Rails:
# Using kaminari or will_paginate@posts = Post.page(params[:page]).per(15)
# In view<%= paginate @posts %>Laravel:
<?php// Controller$posts = Post::paginate(15);// Or cursor pagination$posts = Post::cursorPaginate(15);
return view('posts.index', compact('posts'));
// In Blade view{{ $posts->links() }}
// API responsereturn response()->json($posts);// Includes: data, current_page, last_page, per_page, total, etc.12. Mass Assignment
Section titled “12. Mass Assignment”Rails:
# Controller - strong parametersdef user_params params.require(:user).permit(:name, :email, :role)end
def create @user = User.create(user_params)endLaravel:
<?php// Model - fillable/guardedclass User extends Model{ protected $fillable = ['name', 'email', 'role']; // Or: protected $guarded = ['id', 'admin'];}
// Controllerpublic function store(Request $request){ $user = User::create($request->validated());}13. Query Performance
Section titled “13. Query Performance”Select Specific Columns
Section titled “Select Specific Columns”Rails:
# Instead of SELECT *users = User.select(:id, :name, :email)Laravel:
<?php// Instead of SELECT *$users = User::select(['id', 'name', 'email'])->get();// Or$users = User::get(['id', 'name', 'email']);Chunk Large Datasets
Section titled “Chunk Large Datasets”Rails:
User.find_each(batch_size: 100) do |user| # Process userendLaravel:
<?phpUser::chunk(100, function ($users) { foreach ($users as $user) { // Process user }});
// Or lazy collection (PHP 8+)User::lazy()->each(function ($user) { // Process user});14. Database Migrations
Section titled “14. Database Migrations”Creating Tables
Section titled “Creating Tables”Rails:
class CreatePosts < ActiveRecord::Migration[7.0] def change create_table :posts do |t| t.string :title, null: false t.text :body t.string :slug, index: { unique: true } t.boolean :published, default: false t.integer :views, default: 0 t.references :user, null: false, foreign_key: true
t.timestamps end
add_index :posts, :published endendLaravel:
<?phpuse Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration{ public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body')->nullable(); $table->string('slug')->unique(); $table->boolean('published')->default(false); $table->integer('views')->default(0); $table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
$table->index('published'); }); }
public function down() { Schema::dropIfExists('posts'); }};Modifying Tables
Section titled “Modifying Tables”Rails:
class AddSlugToPosts < ActiveRecord::Migration[7.0] def change add_column :posts, :slug, :string add_index :posts, :slug, unique: true endendLaravel:
<?phpreturn new class extends Migration{ public function up() { Schema::table('posts', function (Blueprint $table) { $table->string('slug')->after('title'); $table->unique('slug'); }); }
public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropUnique(['slug']); $table->dropColumn('slug'); }); }};15. Seeders
Section titled “15. Seeders”Rails:
User.create!( name: 'Admin', email: 'admin@example.com', role: 'admin')
10.times do |i| Post.create!( title: "Post #{i}", body: "Content for post #{i}", user: User.first )endLaravel:
<?phpnamespace Database\Seeders;
use App\Models\User;use App\Models\Post;use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder{ public function run() { User::create([ 'name' => 'Admin', 'email' => 'admin@example.com', 'role' => 'admin', ]);
for ($i = 0; $i < 10; $i++) { Post::create([ 'title' => "Post {$i}", 'body' => "Content for post {$i}", 'user_id' => User::first()->id, ]); } }}
// Run: php artisan db:seed16. Factories (Testing)
Section titled “16. Factories (Testing)”Rails (FactoryBot):
FactoryBot.define do factory :user do name { "John Doe" } email { "john@example.com" } role { "user" }
trait :admin do role { "admin" } end endend
# Usageuser = create(:user)admin = create(:user, :admin)users = create_list(:user, 5)Laravel:
<?phpnamespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory{ public function definition() { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'role' => 'user', ]; }
public function admin() { return $this->state(fn (array $attributes) => [ 'role' => 'admin', ]); }}
// Usage in tests$user = User::factory()->create();$admin = User::factory()->admin()->create();$users = User::factory()->count(5)->create();Wrap-up
Section titled “Wrap-up”You’ve now mastered Eloquent ORM and how it compares to ActiveRecord:
- ✓ Model basics - Eloquent models follow the same Active Record pattern as Rails
- ✓ Relationships -
hasMany,belongsTo,belongsToMany, and polymorphic relationships work just like Rails associations - ✓ Dependent options - Cascading deletes handled in migrations (
cascadeOnDelete()) or model events - ✓ Query building - Chainable queries with explicit
->get()execution (more explicit than Rails) - ✓ Scopes - Reusable query logic with
scope*methods - ✓ Global scopes - Apply default conditions automatically (equivalent to Rails
default_scope) - ✓ Touch associations - Use
$touchesproperty to update parent timestamps automatically - ✓ Eager loading - Use
with()to prevent N+1 queries (equivalent toincludes) - ✓ Migrations - Database schema version control with Laravel migrations
- ✓ Model events - Use
booted()method for callbacks (equivalent to ActiveRecord callbacks) - ✓ Soft deletes - Built-in support with
SoftDeletestrait - ✓ Enums - PHP 8.1+ native enums work seamlessly with Eloquent casting
- ✓ Model validation - Possible at model level, but Laravel prefers Form Request validation
- ✓ Mass assignment - Use
$fillableor$guarded(Laravel) vs strong parameters (Rails controllers) - ✓ Factories - Test data generation similar to FactoryBot
- ✓ Advanced patterns - Transactions, pagination, chunking, and performance optimization
The core patterns are identical between Eloquent and ActiveRecord. The main differences are PHP syntax and Laravel’s explicit query execution (->get(), ->first()) compared to Rails’ implicit execution.
You now have the foundation to work with databases in Laravel using all the ActiveRecord patterns you already know!
Practice Exercise
Section titled “Practice Exercise”Convert this Rails model to Laravel:
class Article < ApplicationRecord belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_and_belongs_to_many :categories
scope :published, -> { where(published: true) } scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
validates :title, presence: true, length: { minimum: 5 } validates :slug, uniqueness: true
before_save :generate_slug
def read_time (body.split.size / 200.0).ceil end
private
def generate_slug self.slug ||= title.parameterize endendCreate the equivalent Eloquent model with:
- Relationships
- Scopes
- Model events
- Accessor for
read_time
Further Reading
Section titled “Further Reading”- Laravel Eloquent Documentation — Official documentation for Eloquent ORM, relationships, and query building
- Laravel Database Migrations — Complete guide to database migrations and schema building
- Laravel Database Factories — Guide to creating test data with factories
- Laravel Soft Deletes — Documentation for soft delete functionality
- Laravel Query Builder — Advanced query building techniques
- Laravel Relationships — Comprehensive guide to all relationship types
- Laravel Model Events — Complete reference for model lifecycle events
- Laravel Database Transactions — Guide to using database transactions
- Active Record Pattern — Wikipedia article explaining the Active Record pattern
::: tip Continue Learning Move on to Chapter 06: Building REST APIs: From Rails to Laravel to learn about API development. :::