Eloquent ORM Basics
Eloquent is Laravel's elegant and powerful ORM (Object-Relational Mapping) implementation. It uses the Active Record pattern, allowing you to work with databas
Eloquent is Laravel's elegant and powerful ORM (Object-Relational Mapping) implementation. It uses the Active Record pattern, allowing you to work with databases using an intuitive, object-oriented syntax. Each database table has a corresponding "Model" that interacts with that table.
Advantages:
Using Artisan:
1# Create a model
2php artisan make:model Post
3
4# Create model with migration
5php artisan make:model Post -m
6
7# Create model with migration, factory, and seeder
8php artisan make:model Post -mfs
9
10# Create model with all resources (migration, factory, seeder, controller)
11php artisan make:model Post --allBasic Model Structure:
1namespace App\Models;
2
3use Illuminate\Database\Eloquent\Model;
4
5class Post extends Model
6{
7 // Model code here
8}Table Name:
1class Post extends Model
2{
3 // By default, Eloquent uses snake_case plural of class name
4 // Post -> posts, UserProfile -> user_profiles
5
6 // Custom table name
7 protected $table = 'my_posts';
8}Primary Key:
1class Post extends Model
2{
3 // Default primary key is 'id'
4 protected $primaryKey = 'post_id';
5
6 // If primary key is not an integer
7 protected $keyType = 'string';
8
9 // If primary key is not auto-incrementing
10 public $incrementing = false;
11}Timestamps:
1class Post extends Model
2{
3 // Disable timestamps (created_at, updated_at)
4 public $timestamps = false;
5
6 // Custom timestamp column names
7 const CREATED_AT = 'creation_date';
8 const UPDATED_AT = 'last_update';
9}Database Connection:
1class Post extends Model
2{
3 // Use a different database connection
4 protected $connection = 'mysql2';
5}Using save():
1// Create new instance
2$post = new Post();
3$post->title = 'My First Post';
4$post->content = 'This is the content';
5$post->published = true;
6$post->save();
7
8echo "Created post with ID: {$post->id}";Using create():
1// Mass assignment
2$post = Post::create([
3 'title' => 'My Second Post',
4 'content' => 'More content',
5 'published' => true,
6]);
7
8// Requires fillable or guarded property
9class Post extends Model
10{
11 // Allow mass assignment for these fields
12 protected $fillable = ['title', 'content', 'published'];
13
14 // OR protect these fields from mass assignment
15 protected $guarded = ['id', 'created_at', 'updated_at'];
16
17 // OR allow all fields (dangerous!)
18 protected $guarded = [];
19}firstOrCreate / firstOrNew:
1// Find first matching record or create it
2$post = Post::firstOrCreate(
3 ['title' => 'Unique Title'],
4 ['content' => 'Default content']
5);
6
7// Find first matching record or create a new instance (not saved)
8$post = Post::firstOrNew(
9 ['title' => 'Unique Title'],
10 ['content' => 'Default content']
11);updateOrCreate:
1// Update existing record or create new one
2$post = Post::updateOrCreate(
3 ['slug' => 'my-post'], // Search criteria
4 ['title' => 'Updated Title', 'content' => 'Updated content'] // Values to update/create
5);Retrieving All Records:
1// Get all posts
2$posts = Post::all();
3
4foreach ($posts as $post) {
5 echo $post->title . "\n";
6}
7
8// Get specific columns
9$posts = Post::all(['title', 'published']);Finding by Primary Key:
1// Find by ID (returns null if not found)
2$post = Post::find(1);
3
4// Find multiple by IDs
5$posts = Post::find([1, 2, 3]);
6
7// Find or throw 404 exception
8$post = Post::findOrFail(1);
9
10// Find or execute callback
11$post = Post::findOr(1, function () {
12 // Not found, create new one
13 return Post::create(['title' => 'Default']);
14});Query Constraints:
1// Where clauses
2$posts = Post::where('published', true)->get();
3
4$posts = Post::where('views', '>', 100)->get();
5
6$posts = Post::where('title', 'like', '%Laravel%')->get();
7
8// Multiple where clauses
9$posts = Post::where('published', true)
10 ->where('views', '>', 100)
11 ->get();
12
13// Or where
14$posts = Post::where('published', true)
15 ->orWhere('featured', true)
16 ->get();
17
18// Where in
19$posts = Post::whereIn('category_id', [1, 2, 3])->get();
20
21// Where between
22$posts = Post::whereBetween('views', [100, 1000])->get();
23
24// Where null
25$posts = Post::whereNull('deleted_at')->get();
26
27// Where date
28$posts = Post::whereDate('created_at', '2025-01-01')->get();Ordering and Limiting:
1// Order by
2$posts = Post::orderBy('created_at', 'desc')->get();
3
4// Multiple orders
5$posts = Post::orderBy('published', 'desc')
6 ->orderBy('created_at', 'desc')
7 ->get();
8
9// Latest/Oldest (by created_at)
10$posts = Post::latest()->get();
11$posts = Post::oldest()->get();
12
13// Limit
14$posts = Post::take(10)->get();
15$posts = Post::limit(10)->get();
16
17// Skip and take
18$posts = Post::skip(10)->take(5)->get();Getting First/Single Record:
1// Get first record
2$post = Post::first();
3
4// Get first matching record
5$post = Post::where('published', true)->first();
6
7// Get first or fail
8$post = Post::where('slug', 'my-post')->firstOrFail();
9
10// Get single record (throws exception if multiple found)
11$post = Post::where('slug', 'unique-slug')->sole();Using save():
1$post = Post::find(1);
2$post->title = 'Updated Title';
3$post->save();Using update():
1// Update single record
2$post = Post::find(1);
3$post->update(['title' => 'New Title']);
4
5// Mass update
6Post::where('published', false)
7 ->update(['published' => true]);Increment/Decrement:
1// Increment views by 1
2$post->increment('views');
3
4// Increment by specific amount
5$post->increment('views', 5);
6
7// Increment with additional updates
8$post->increment('views', 1, ['last_viewed_at' => now()]);
9
10// Decrement
11$post->decrement('views');Delete Instance:
1$post = Post::find(1);
2$post->delete();Delete by Key:
1// Delete single record
2Post::destroy(1);
3
4// Delete multiple records
5Post::destroy([1, 2, 3]);
6Post::destroy(1, 2, 3);Mass Delete:
1// Delete all matching records
2Post::where('published', false)->delete();Define reusable query constraints:
1class Post extends Model
2{
3 // Scope method name must start with 'scope'
4 public function scopePublished($query)
5 {
6 return $query->where('published', true);
7 }
8
9 public function scopePopular($query, $minViews = 100)
10 {
11 return $query->where('views', '>=', $minViews);
12 }
13
14 public function scopeRecent($query, $days = 7)
15 {
16 return $query->where('created_at', '>=', now()->subDays($days));
17 }
18}
19
20// Using scopes
21$posts = Post::published()->get();
22
23$posts = Post::published()->popular()->get();
24
25$posts = Post::published()
26 ->popular(500)
27 ->recent(14)
28 ->get();Apply constraints to all queries:
1namespace App\Models\Scopes;
2
3use Illuminate\Database\Eloquent\Builder;
4use Illuminate\Database\Eloquent\Model;
5use Illuminate\Database\Eloquent\Scope;
6
7class PublishedScope implements Scope
8{
9 public function apply(Builder $builder, Model $model): void
10 {
11 $builder->where('published', true);
12 }
13}
14
15// Apply to model
16class Post extends Model
17{
18 protected static function booted(): void
19 {
20 static::addGlobalScope(new PublishedScope);
21
22 // Or use closure
23 static::addGlobalScope('active', function (Builder $builder) {
24 $builder->where('active', true);
25 });
26 }
27}
28
29// All queries automatically filtered
30$posts = Post::all(); // Only published posts
31
32// Remove global scope
33$posts = Post::withoutGlobalScope(PublishedScope::class)->get();
34
35// Remove multiple scopes
36$posts = Post::withoutGlobalScopes([
37 PublishedScope::class,
38 'active'
39])->get();Example: User has one Profile
1// User model
2class User extends Model
3{
4 public function profile()
5 {
6 return $this->hasOne(Profile::class);
7
8 // With custom foreign key
9 // return $this->hasOne(Profile::class, 'user_id', 'id');
10 }
11}
12
13// Profile model
14class Profile extends Model
15{
16 public function user()
17 {
18 return $this->belongsTo(User::class);
19 }
20}
21
22// Usage
23$user = User::find(1);
24$profile = $user->profile; // Get associated profile
25
26echo $profile->bio;
27
28// Inverse
29$profile = Profile::find(1);
30$user = $profile->user; // Get associated userExample: Post has many Comments
1// Post model
2class Post extends Model
3{
4 public function comments()
5 {
6 return $this->hasMany(Comment::class);
7 }
8}
9
10// Comment model
11class Comment extends Model
12{
13 public function post()
14 {
15 return $this->belongsTo(Post::class);
16 }
17}
18
19// Usage
20$post = Post::find(1);
21$comments = $post->comments; // Collection of comments
22
23foreach ($comments as $comment) {
24 echo $comment->content . "\n";
25}
26
27// Add new comment
28$comment = new Comment(['content' => 'Great post!']);
29$post->comments()->save($comment);
30
31// Or
32$post->comments()->create(['content' => 'Another comment']);Example: User has many Roles, Role has many Users
Database Structure:
users tableroles tablerole_user pivot table (foreign keys: user_id, role_id)1// User model
2class User extends Model
3{
4 public function roles()
5 {
6 return $this->belongsToMany(Role::class);
7
8 // With custom pivot table and columns
9 // return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
10 }
11}
12
13// Role model
14class Role extends Model
15{
16 public function users()
17 {
18 return $this->belongsToMany(User::class);
19 }
20}
21
22// Usage
23$user = User::find(1);
24$roles = $user->roles; // Collection of roles
25
26// Attach roles
27$user->roles()->attach(1); // Add role ID 1
28$user->roles()->attach([2, 3]); // Add multiple roles
29
30// Attach with pivot data
31$user->roles()->attach(1, ['expires_at' => now()->addYear()]);
32
33// Detach roles
34$user->roles()->detach(1); // Remove role ID 1
35$user->roles()->detach([2, 3]); // Remove multiple
36$user->roles()->detach(); // Remove all roles
37
38// Sync (replace all roles)
39$user->roles()->sync([1, 2, 3]);
40
41// Toggle
42$user->roles()->toggle([1, 2]);Accessing Pivot Data:
1$user = User::find(1);
2
3foreach ($user->roles as $role) {
4 echo $role->name;
5 echo $role->pivot->created_at; // Access pivot columns
6}
7
8// Include additional pivot columns
9class User extends Model
10{
11 public function roles()
12 {
13 return $this->belongsToMany(Role::class)
14 ->withPivot('expires_at', 'granted_by')
15 ->withTimestamps();
16 }
17}Example: Country has many Posts through Users
1class Country extends Model
2{
3 public function posts()
4 {
5 return $this->hasManyThrough(
6 Post::class, // Final model
7 User::class, // Intermediate model
8 'country_id', // Foreign key on users table
9 'user_id', // Foreign key on posts table
10 'id', // Local key on countries table
11 'id' // Local key on users table
12 );
13 }
14}
15
16// Usage
17$country = Country::find(1);
18$posts = $country->posts; // All posts from users in this countryProblem:
1// N+1 queries: 1 for posts + N for each post's author
2$posts = Post::all();
3
4foreach ($posts as $post) {
5 echo $post->author->name; // Query for each post!
6}Solution: Eager Loading
1// 2 queries total: 1 for posts + 1 for all authors
2$posts = Post::with('author')->get();
3
4foreach ($posts as $post) {
5 echo $post->author->name; // No additional query
6}
7
8// Multiple relationships
9$posts = Post::with(['author', 'comments', 'tags'])->get();
10
11// Nested relationships
12$posts = Post::with('comments.author')->get();
13
14foreach ($posts as $post) {
15 foreach ($post->comments as $comment) {
16 echo $comment->author->name; // Already loaded
17 }
18}Load relationships after retrieving model:
1$posts = Post::all();
2
3// Later, load relationship
4$posts->load('author');
5
6// Load multiple
7$posts->load(['author', 'comments']);1// Only load published comments
2$posts = Post::with(['comments' => function ($query) {
3 $query->where('approved', true)
4 ->orderBy('created_at', 'desc');
5}])->get();
6
7// Load only specific columns
8$posts = Post::with('author:id,name,email')->get();Transform attribute values when retrieving:
1class User extends Model
2{
3 // Accessor for 'name' attribute
4 protected function name(): Attribute
5 {
6 return Attribute::make(
7 get: fn (string $value) => ucfirst($value),
8 );
9 }
10
11 // Virtual attribute (doesn't exist in database)
12 protected function fullName(): Attribute
13 {
14 return Attribute::make(
15 get: fn () => "{$this->first_name} {$this->last_name}",
16 );
17 }
18}
19
20// Usage
21$user = User::find(1);
22echo $user->name; // Automatically capitalized
23echo $user->full_name; // Concatenated first and last nameTransform attribute values when setting:
1class User extends Model
2{
3 // Mutator for 'name' attribute
4 protected function name(): Attribute
5 {
6 return Attribute::make(
7 get: fn (string $value) => ucfirst($value),
8 set: fn (string $value) => strtolower($value),
9 );
10 }
11
12 // Hash password when setting
13 protected function password(): Attribute
14 {
15 return Attribute::make(
16 set: fn (string $value) => bcrypt($value),
17 );
18 }
19}
20
21// Usage
22$user = new User();
23$user->name = 'JOHN DOE'; // Stored as 'john doe'
24$user->password = 'secret'; // Stored as hashed value
25$user->save();
26
27echo $user->name; // Retrieved as 'John doe' (ucfirst applied)Automatically cast attributes to specific types:
1class Post extends Model
2{
3 protected $casts = [
4 'published' => 'boolean',
5 'published_at' => 'datetime',
6 'views' => 'integer',
7 'rating' => 'float',
8 'options' => 'array', // JSON to array
9 'metadata' => 'object', // JSON to object
10 'config' => 'collection', // JSON to Collection
11 ];
12}
13
14// Usage
15$post = Post::find(1);
16
17// Automatically cast
18var_dump($post->published); // bool(true) not string "1"
19
20// Dates are Carbon instances
21echo $post->published_at->format('Y-m-d');
22echo $post->published_at->diffForHumans(); // "2 days ago"
23
24// Arrays and objects
25$post->options = ['featured' => true, 'pinned' => false];
26$post->save(); // Automatically JSON encoded
27
28$options = $post->options; // Automatically decoded to array1namespace App\Casts;
2
3use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
4
5class Json implements CastsAttributes
6{
7 public function get($model, string $key, $value, array $attributes)
8 {
9 return json_decode($value, true);
10 }
11
12 public function set($model, string $key, $value, array $attributes)
13 {
14 return json_encode($value);
15 }
16}
17
18// Use in model
19class Post extends Model
20{
21 protected $casts = [
22 'options' => Json::class,
23 ];
24}Keep deleted records in database:
1use Illuminate\Database\Eloquent\SoftDeletes;
2
3class Post extends Model
4{
5 use SoftDeletes;
6
7 // Optional: customize deleted_at column name
8 // const DELETED_AT = 'removed_at';
9}
10
11// Migration must include deleted_at column
12Schema::create('posts', function (Blueprint $table) {
13 $table->id();
14 $table->string('title');
15 $table->text('content');
16 $table->softDeletes(); // Adds deleted_at column
17 $table->timestamps();
18});
19
20// Usage
21$post = Post::find(1);
22$post->delete(); // Sets deleted_at timestamp, doesn't remove row
23
24// Queries automatically exclude soft-deleted
25$posts = Post::all(); // Doesn't include soft-deleted
26
27// Include soft-deleted
28$posts = Post::withTrashed()->get();
29
30// Only soft-deleted
31$posts = Post::onlyTrashed()->get();
32
33// Restore soft-deleted
34$post = Post::withTrashed()->find(1);
35$post->restore();
36
37// Permanently delete
38$post = Post::withTrashed()->find(1);
39$post->forceDelete();
40
41// Check if soft-deleted
42if ($post->trashed()) {
43 // Post is soft-deleted
44}1// Good: Explicit
2$user = User::with('posts')->find(1);
3foreach ($user->posts as $post) {
4 // ...
5}
6
7// Bad: Lazy loading (N+1 problem)
8$user = User::find(1);
9foreach ($user->posts as $post) { // Query for each iteration
10 // ...
11}1// Process large datasets efficiently
2Post::chunk(100, function ($posts) {
3 foreach ($posts as $post) {
4 // Process post
5 }
6});
7
8// Or lazy() for memory efficiency
9foreach (Post::lazy() as $post) {
10 // Process one at a time
11}1// Good: Reusable scope
2class Post extends Model
3{
4 public function scopeActive($query)
5 {
6 return $query->where('published', true)
7 ->where('published_at', '<=', now());
8 }
9}
10
11$posts = Post::active()->get();
12
13// Bad: Duplicate query logic everywhere
14$posts = Post::where('published', true)
15 ->where('published_at', '<=', now())
16 ->get();1class Post extends Model
2{
3 // Whitelist approach (recommended)
4 protected $fillable = ['title', 'content', 'published'];
5
6 // Blacklist approach
7 protected $guarded = ['id', 'user_id'];
8
9 // Never do this in production
10 // protected $guarded = [];
11}Always index foreign key columns:
1Schema::create('posts', function (Blueprint $table) {
2 $table->id();
3 $table->foreignId('user_id')->constrained()->cascadeOnDelete();
4 // Automatically creates index on user_id
5});Eloquent ORM provides a beautiful, fluent interface for database interactions:
$fillable or $guarded for mass assignment protectionEloquent makes database operations enjoyable while maintaining performance and security!
Your progress is saved in this browser, on this device only.