Laravel Eloquent ORM: Beginner Guide with Advanced Examples
Contents
- 1 Laravel Eloquent ORM: Beginner Guide (2025)
- 2 Setting Up Laravel 12 Environment
- 3 Creating a Model with Migration
- 4 Running the Migration
- 5 Basic Eloquent Operations
- 6 Query Scopes
- 7 Advanced Where Clauses
- 8 Aggregates and Calculations
- 9 Query Optimization
- 9.1 Select Specific Columns Instead of All
- 9.2 Use pluck() for Single Column Values
- 9.3 Use exists() for Efficient Conditional Checks
- 9.4 Use value() Method for Single Values
- 9.5 Leverage where() Optimization Methods
- 9.6 Use first() Method for Single Row Return
- 9.7 Eloquent Ordering Methods
- 9.8 Use Chunking for Large Datasets
- 9.9 Eloquent Find Optimizations
- 9.10 Eloquent Conditional Queries
- 10 Conclusion
Laravel Eloquent ORM: Beginner Guide (2025)
Laravel Eloquent ORM (Object-Relational Mapping) allows developers to interact with the database without writing raw SQL queries. With Eloquent, every database table has a corresponding “Model” that lets you query, insert, update, and delete data using fluent PHP methods.
You can think of it as a bridge that translates your PHP code into database commands. In this complete guide, we’ll go step-by-step from creating models and migrations to performing advanced Eloquent queries and optimizing performance.
Setting Up Laravel 12 Environment
Before we dive into Eloquent models, we’ll set up a fresh Laravel 12 installation with proper database configuration.
composer create-project laravel/laravel eloquent-tutorial "^12.0"
cd eloquent-tutorial
cp .env.example .env
php artisan key:generate
We’ll configure our database connection in the .env
file:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=eloquent_tutorial
DB_USERNAME=root
DB_PASSWORD=your_password
Creating a Model with Migration
In Laravel, a Model is a PHP class that represents a database table. When creating a model, we usually create a migration alongside it — which acts like a blueprint for the database table structure.
Run the following Artisan command to create a model and its migration file:
php artisan make:model User -m
This does two things:
- Creates
User.php
inapp/Models
— our Eloquent model file. - Creates a migration file in
database/migrations
to define the table structure.
Example of the User
model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'phone',
'address'
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
}
}
The $fillable
array specifies which attributes can be mass-assigned, providing protection against mass assignment vulnerabilities. The $hidden
array excludes sensitive fields from JSON serialization.
The casts()
method automatically converts database values into PHP types. For example, email_verified_at
is cast to a datetime
object, password
is automatically hashed when saved, and timestamps are cast to datetime
objects for easy date manipulation.
Example migration for the users
table:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('phone')->nullable();
$table->text('address')->nullable();
$table->rememberToken();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
In this migration, the unique()
method on the email
column ensures that each email address in the database is stored only once, preventing duplicate registrations. The nullable()
method allows certain fields, such as phone
and address
, to be left empty if the user chooses not to provide them.
The rememberToken()
method is used by Laravel’s authentication system to store a token for the “remember me” feature.
Finally, timestamps()
automatically manages the created_at
and updated_at
fields so that we don’t have to manually handle them when creating or updating a record.
Running the Migration
Now that we have our model and migration ready, it’s time to create the actual table in the database. Laravel makes this easy with a single Artisan command:
php artisan migrate
When this command runs, Laravel looks at all the migration files in the database/migrations
folder that haven’t been executed yet and applies them to the database. In our case, this will create the users
table with all the columns we defined.
If you ever need to roll back a migration — for example, to modify a table structure — you can use:
php artisan migrate:rollback
This will undo the last batch of migrations, allowing you to make adjustments and run them again.
Basic Eloquent Operations
With our model and table in place, we can start performing CRUD (Create, Read, Update, Delete) operations using Eloquent. One of the biggest advantages of Eloquent is that these operations are written in clean, readable PHP, yet Laravel translates them into efficient SQL queries.
Creating Records
There are multiple approaches to insert new records into the database, each suited for different scenarios and use cases:
<?php
// Method 1: Using create() method
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => bcrypt('password123'),
'phone' => '+1234567890'
]);
// Method 2: Using new instance
$user = new User();
$user->name = 'Jane Smith';
$user->email = 'jane@example.com';
$user->password = bcrypt('password123');
$user->save();
// Method 3: Using firstOrCreate()
$user = User::firstOrCreate(
['email' => 'bob@example.com'],
[
'name' => 'Bob Wilson',
'password' => bcrypt('password123'),
'phone' => '+1987654321'
]
);
The create()
method performs mass assignment and immediately saves the record to the database. Creating a new instance and calling save()
gives more control over the process, allowing validation or additional logic before saving.
Whereas, the firstOrCreate()
method checks if a record exists with the given conditions and creates it only if it doesn’t exist, preventing duplicates.
Reading Records
Next, let’s discover various methods to retrieve data from the database, from simple queries to complex conditions:
<?php
// Find by primary key
$user = User::find(1);
$users = User::find([1, 2, 3]);
// Find or fail (throws exception if not found)
$user = User::findOrFail(1);
// Get all records
$users = User::all();
// Get with conditions
$users = User::where('email', 'like', '%@gmail.com')->get();
// Get first record
$user = User::where('name', 'John')->first();
// Get or create
$user = User::firstOrCreate(['email' => 'new@example.com']);
// Pagination
$users = User::paginate(15);
$users = User::simplePaginate(10);
The find()
method retrieves records by their primary key and returns null if not found. findOrFail()
throws a ModelNotFoundException
if the record doesn’t exist, useful for ensuring data exists.
The where()
method builds query conditions, while get()
executes the query and returns a collection. Pagination methods help manage large datasets by splitting results across multiple pages.
Updating Records
There are different approaches to modify existing records, including single record updates and mass updates:
<?php
// Update single record
$user = User::find(1);
$user->name = 'Updated Name';
$user->save();
// Mass update
User::where('created_at', '<', now()->subDays(30))
->update(['status' => 'inactive']);
// Update or create
User::updateOrCreate(
['email' => 'update@example.com'],
[
'name' => 'Updated User',
'phone' => '+1122334455'
]
);
// Using fill method
$user = User::find(1);
$user->fill([
'name' => 'New Name',
'phone' => '+1555666777'
]);
$user->save();
The single record updates retrieve the model instance, modify attributes, and save changes. Whereas, the mass updates apply changes to multiple records matching specific conditions without loading them into memory, making them efficient for bulk operations.
In addition, the updateOrCreate()
method updates an existing record or creates a new one if it doesn’t exist. The fill()
method assigns multiple attributes at once while respecting the $fillable
property.
Deleting Records
Then we’ll explore various deletion methods including soft deletes, which provide a safety net for recovering accidentally deleted data:
<?php
// Delete single record
$user = User::find(1);
$user->delete();
// Delete by ID
User::destroy(1);
User::destroy([1, 2, 3]);
// Conditional delete
User::where('email_verified_at', null)
->where('created_at', '<', now()->subDays(7))
->delete();
// Soft delete (if using SoftDeletes trait)
$user = User::find(1);
$user->delete(); // Soft delete
// Restore soft deleted
$user = User::withTrashed()->find(1);
$user->restore();
// Force delete (permanent)
$user->forceDelete();
The delete()
method removes a single record, while destroy()
can delete multiple records by ID. Conditional deletes remove records matching specific criteria.
In addition, the soft deletes mark records as deleted without actually removing them from the database, allowing recovery. The withTrashed()
scope includes soft-deleted records in queries, and restore()
brings back soft-deleted records.
Query Scopes
Query scopes help encapsulate common query logic within models, making code more organized and reusable across different parts of applications
Local Scopes
Local scopes are methods defined within our model that start with the scope
prefix.
public function scope{ScopeName}($query, $parameter1, $parameter2, ...)
{
return $query->where(...);
}
The method name must start with scope
followed by the scope name in PascalCase.
<?php
class User extends Authenticatable
{
public function scopeActive($query)
{
return $query->where('status', 'active');
}
public function scopeCreatedBetween($query, $startDate, $endDate)
{
return $query->whereBetween('created_at', [$startDate, $endDate]);
}
public function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
}
// Usage examples
$activeUsers = User::active()->get();
$recentUsers = User::createdBetween('2024-01-01', '2024-12-31')->get();
$verifiedActiveUsers = User::active()->verified()->get();
When called, Laravel automatically removes the scope
prefix and converts the method to camelCase. These scopes can accept parameters for dynamic filtering and can be chained together with other scopes and Eloquent methods.
The first parameter $query
represents the current query builder instance, and the method must return the modified query builder to enable method chaining.
Global Scopes
Global scopes are automatically applied to all queries for a model. They’re useful for soft deletes, multi-tenancy, or filtering by status.
First, we create a global scope class:
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class ActiveScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('status', 'active');
}
}
Then we can apply the global scope to a model:
<?php
namespace App\Models;
use App\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected static function booted()
{
static::addGlobalScope(new ActiveScope);
}
}
$users = User::all(); // SELECT * FROM users WHERE status = 'active'
$user = User::find(1); // SELECT * FROM users WHERE id = 1 AND status = 'active'
With this all queries will automatically include the active status filter on a model.
Global scopes are registered in the model’s booted
method and can be temporarily disabled using withoutGlobalScope()
methods.
<?php
class User extends Model
{
protected static function booted()
{
static::addGlobalScope('active', function (Builder $builder) {
$builder->where('status', 'active');
});
}
}
$users = User::withoutGlobalScope(ActiveScope::class)->get();
// Remove multiple global scopes
$users = User::withoutGlobalScopes([
ActiveScope::class,
VerifiedScope::class
])->get();
// Remove all global scopes
$users = User::withoutGlobalScopes()->get();
Advanced Where Clauses
Sometimes, our queries need more complex conditions than a simple where()
statement. Laravel Eloquent provides powerful methods to build advanced conditions while still keeping the syntax clean.
For example, if we want users who are active and registered after January 1st, 2024:
$users = User::where('status', 'active')
->where('created_at', '>=', '2024-01-01')
->get();
If we need OR conditions between clauses, for example users who are active or whose email is verified:
$users = User::where('status', 'active')
->orWhere('email_verified_at', '!=', null)
->get();
We can also group conditions for more precise control:
$users = User::where(function ($query) {
$query->where('status', 'active')
->where('email_verified_at', '!=', null);
})
->orWhere('role', 'admin')
->get();
This query finds users who are both active and verified, or users who have the role of admin
. The closure groups the AND
conditions together, while the orWhere
applies outside the group. Laravel handles the SQL parentheses automatically.
To filter users where the id
is within a list of specific IDs:
$users = User::whereIn('id', [1, 2, 3, 4, 5])->get();
To exclude users with certain statuses, for example 'banned'
or 'suspended'
:
$users = User::whereNotIn('status', ['banned', 'suspended'])->get();
To find users who have not verified their email (i.e., the email_verified_at
is NULL
):
$unverifiedUsers = User::whereNull('email_verified_at')->get();
To find users who have verified their email (non-null email_verified_at
):
$verifiedUsers = User::whereNotNull('email_verified_at')->get();
For date-specific queries, to find users created exactly today:
$todayUsers = User::whereDate('created_at', today())->get();
To find users created in the current month:
$thisMonthUsers = User::whereMonth('created_at', now()->month)->get();
To find users created in the current year:
$thisYearUsers = User::whereYear('created_at', now()->year)->get();
To query JSON columns, for example checking if the JSON array preferences->notifications
contains 'email'
:
$users = User::whereJsonContains('preferences->notifications', 'email')->get();
To get exactly one user by email, and throw an exception if not found or multiple found:
$user = User::where('email', 'john@example.com')->sole();
To efficiently process large datasets in chunks ordered by primary key:
User::lazyById(100)->each(function ($user) {
// process each user
});
Below are common SQL patterns and their Eloquent equivalents.
SQL Query | Eloquent Equivalent |
---|---|
SELECT * FROM users WHERE active = 1 AND age > 18 | User::where('active', 1) ->where('age', '>', 18) ->get(); |
SELECT * FROM users WHERE status = 'admin' OR status = 'editor' | User::where('status', 'admin') ->orWhere('status', 'editor') ->get(); |
SELECT * FROM users WHERE (active = 1 AND age > 18) OR (created_at > '2024-01-01') | User::where(function($query) { $query->where('active', 1) ->where('age', '>', 18); })->orWhere('created_at', '>', '2024-01-01') ->get(); |
SELECT * FROM users WHERE deleted_at IS NULL | User::whereNull('deleted_at')->get() |
SELECT * FROM users WHERE email_verified_at IS NOT NULL | User::whereNotNull('email_verified_at')->get() |
SELECT * FROM users WHERE id IN (1, 2, 3) | User::whereIn('id', [1, 2, 3])->get() |
SELECT * FROM users WHERE role NOT IN ('admin', 'editor') | User::whereNotIn('role', ['admin', 'editor'])->get() |
Aggregates and Calculations
Eloquent provides aggregate methods like count()
, max()
, min()
, avg()
, and sum()
for quick statistics:
<?php
// Counting
$totalUsers = User::count();
$activeUsers = User::where('status', 'active')->count();
// Maximum and Minimum
$newestUserId = User::max('id');
$oldestUser = User::min('created_at');
// Average and Sum
$averageAge = User::avg('age');
$totalOrders = Order::sum('total_amount');
// Group By with aggregates
$usersByStatus = User::select('status', DB::raw('count(*) as total'))
->groupBy('status')
->get();
// Having clause
$activeRegions = User::select('region', DB::raw('count(*) as user_count'))
->groupBy('region')
->having('user_count', '>', 10)
->get();
Aggregate methods like count()
, max()
, min()
, avg()
, and sum()
allow us to perform statistical operations directly with Eloquent without writing raw SQL. They can be chained with other query constraints for powerful data insights.
Grouping (groupBy
) and filtering groups (having
) enable complex reporting and summaries. Laravel’s DB::raw()
is used here to add raw SQL expressions like count(*)
when needed.s.
Query Optimization
Laravel Eloquent provides numerous built-in features designed specifically for query optimization.
Select Specific Columns Instead of All
The select()
method tells Eloquent to fetch only the columns you specify instead of using SELECT *
. This reduces memory usage, network transfer time, and database I/O:
$users = User::select('id', 'name', 'email')->get();
We can combine select()
with other query methods seamlessly. The database engine can also optimize queries better when it knows exactly which columns to retrieve, potentially using covering indexes that contain all requested columns.
$products = Product::select('id', 'name', 'price')
->where('status', 'active')
->get();
Use pluck() for Single Column Values
The pluck()
method extracts a single column’s values directly into a simple array or key-value collection. This is much more memory-efficient than fetching full model instances when we only need one or two column values:
$userNames = User::where('status', 'active')->pluck('name');
$userMap = User::pluck('name', 'id');
Use exists()
for Efficient Conditional Checks
The exists()
method generates an optimized SQL query that stops execution as soon as it finds the first matching record. This is much faster than using count() > 0
or get()->isNotEmpty()
because it doesn’t need to count all matching records or fetch any actual data:
$hasActiveUsers = User::where('status', 'active')->exists();
Database engines can often satisfy EXISTS queries using only index data without accessing the actual table data, making these checks extremely fast even on large tables.
if (Post::where('user_id', $userId)->exists()) {
echo "User has posts";
}
Use value()
Method for Single Values
The value()
method is Eloquent’s most efficient way to retrieve a single column value from a single record. It returns the raw value directly without creating model instances or collections, making it perfect for quick lookups and checks:
$newestUserName = User::latest()->value('name');
$userEmail = User::where('id', $userId)->value('email');
Leverage where()
Optimization Methods
Eloquent’s dynamic where
methods create optimized queries while maintaining readability. Methods like whereDate()
, whereYear()
, and whereTime()
generate database-optimized date comparisons instead of fetching all records and filtering in PHP:
$products = Product::whereName('iPhone')->first();
$activeUsers = User::whereActive(true)->get();
$recentPosts = Post::whereDate('created_at', today())->get();
Use first() Method for Single Row Return
Eloquent’s first()
method automatically adds LIMIT 1
to our SQL query, ensuring optimal performance when you only need a single record. This is more efficient than using get()->first()
which would fetch all matching records then return only the first one:
$user = User::where('email', $email)->first();
$latestOrder = Order::latest()->first();
Eloquent Ordering Methods
Eloquent’s ordering methods like latest()
, oldest()
, and orderByDesc()
generate optimized ORDER BY clauses. These methods can leverage database indexes effectively, especially when combined with column selection and limits:
$recentPosts = Post::latest()->get();
$oldestUsers = User::oldest('created_at')->get();
$topProducts = Product::orderByDesc('rating')->get();
Use Chunking for Large Datasets
The chunk()
method processes large datasets efficiently by fetching records in smaller batches. This prevents memory exhaustion while maintaining Eloquent’s full ORM functionality for each batch of models.
User::where('status', 'inactive')->chunk(100, function ($users) {
foreach ($users as $user) {
$user->sendReactivationEmail();
}
});
Eloquent Find Optimizations
The eloquent’s find()
methods are highly optimized for primary key lookups. They use the most efficient possible query structure and can leverage primary key indexes:
$user = User::find($id);
$users = User::find([$id1, $id2, $id3]);
$user = User::findOrFail($id);
When passing an array to find()
, Eloquent generates a single optimized WHERE IN
query instead of multiple queries
Eloquent Conditional Queries
The when()
method only adds query conditions if the first parameter is truthy. This prevents building inefficient queries with empty or null conditions.
Without when()
– Problems:
- If
$searchTerm
is empty/null, you get:WHERE name LIKE '%%'
(matches everything – wasteful) - If
$status
is null, you get:WHERE status IS NULL
(probably not what you want) - Database has to process unnecessary conditions even when they’re meaningless
With when()
– Efficient:
$query = User::query();
$query->when($searchTerm, function ($query, $searchTerm) {
return $query->where('name', 'like', "%{$searchTerm}%");
});
$query->when($status, function ($query, $status) {
return $query->where('status', $status);
});
$users = $query->get();
- If
$searchTerm
has a value → adds the name search condition - If
$searchTerm
is empty/null → skips this condition entirely - If
$status
has a value → adds the status condition - If
$status
is empty/null → skips this condition entirely
Conclusion
Laravel Eloquent ORM transforms database interactions from complex SQL queries into elegant, readable PHP code. Throughout this guide, we’ve covered everything from basic CRUD operations to advanced query optimization techniques that will make our applications faster and more maintainable.
In our next tutorial, we’ll dive deep into Laravel Eloquent Relationships – the powerful feature that makes working with related data incredibly simple.