Eloquent is Laravel's built-in ORM (Object-Relational Mapping) that makes interacting with a database feel like working with plain PHP objects. Instead of writing long SQL queries, you simply call methods on a model. Mastering Eloquent is what separates developers who write messy code from those who write clean, fast, maintainable Laravel code. This article covers Eloquent thoroughly with real examples.
1. A Model Represents a Table
Every Eloquent model represents one table. By convention, a Post model automatically maps to a posts table (plural, lowercase). Create a model with artisan:
php artisan make:model Post
A minimal model is enough. You rarely need any configuration because Eloquent relies on convention:
class Post extends Model
{
protected $fillable = ['title', 'body', 'user_id', 'published'];
}
The $fillable property defines which columns may be mass-assigned via create() or update(). This protects you from mass-assignment vulnerabilities.
2. Retrieving Data (Read)
Eloquent offers many methods to fetch data. Here are the most common:
$posts = Post::all(); // all rows
$post = Post::find(1); // find by primary key
$post = Post::findOrFail(1); // 404 if missing
$first = Post::where('published', true)->first();
$published = Post::where('published', true)
->orderBy('created_at', 'desc')
->take(10)
->get();
Important difference: get() returns a Collection, while first() returns a single model or null. Use findOrFail() in a controller so Laravel automatically throws a 404 when the record is missing.
3. Create, Update, and Delete
Write operations in Eloquent are very concise:
// Create
$post = Post::create([
'title' => 'Learning Eloquent',
'body' => 'Article content...',
'user_id' => 1,
]);
// Update
$post->update(['title' => 'New Title']);
// Delete
$post->delete();
There is also updateOrCreate(), useful to prevent duplicates: Eloquent looks for a matching row, updates it if found, or creates a new one if not.
Post::updateOrCreate(
['slug' => 'learning-eloquent'], // search conditions
['title' => 'Learning Eloquent', 'body' => '...']
);
4. Relationships Between Tables
Eloquent's real power lies in relationships. Imagine a User has many Posts, and each Post has many Comments.
hasMany and belongsTo
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
Now you can access relationships like normal properties:
$user = User::find(1);
foreach ($user->posts as $post) {
echo $post->title;
}
$post = Post::find(1);
echo $post->user->name; // navigate to the owner
belongsToMany (Many-to-Many)
A many-to-many relationship, for example a Post having many Tags and vice versa, needs a pivot table (usually post_tag):
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
// Attach tags without removing existing ones:
$post->tags()->attach([1, 2, 3]);
// Sync (remove any not in the list):
$post->tags()->sync([2, 3]);
5. The N+1 Problem and Eager Loading
This is a concept you MUST understand. Look at this code:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // 1 EXTRA query per post!
}
With 100 posts, Laravel runs 1 query to fetch the posts + 100 queries to fetch each user = 101 queries. This is the N+1 problem. The fix is eager loading with with():
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // no extra queries
}
Now it is only 2 queries total, no matter how many posts. You can even eager load nested relationships: Post::with('user', 'comments.user')->get().
6. Accessors and Mutators
An accessor transforms a value when it is READ; a mutator transforms it when it is SAVED. In modern Laravel both live in a single method using Attribute:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function title(): Attribute
{
return Attribute::make(
get: fn ($value) => ucfirst($value),
set: fn ($value) => strtolower($value),
);
}
With this, the title is always stored lowercase but displayed capitalized automatically.
7. Query Scopes for Clean Code
Scopes store reusable query fragments so controllers stay concise. Define them with the scope prefix:
public function scopePublished($query)
{
return $query->where('published', true);
}
Call them without the scope prefix, and chain them freely:
$posts = Post::published()->latest()->get();
Best Practices When Using Eloquent
The following habits keep your Eloquent code healthy in the long run:
- Always declare
$fillable: do not rely on$guarded = []in real apps because it opens a mass-assignment hole. - Use casting: convert columns to the right type via the
$castsproperty, for example'published' => 'boolean'or'meta' => 'array', so values are cast automatically. - Pick the right relationship: confusing
hasOneandhasManymakes the data you fetch behave unexpectedly. - Use
findOrFailin controllers: this keeps the app returning a proper 404 instead of a null error.
A very commonly used casting example:
protected $casts = [
'published' => 'boolean',
'published_at' => 'datetime',
'meta' => 'array',
];
With the casts above, a meta column stored as JSON automatically becomes a PHP array when read, and turns back into JSON when saved — without any manual json_encode.
Conclusion
Eloquent makes you think in objects and relationships rather than raw SQL. The keys to mastering it: understand model conventions, use relationships to connect data, and always stay alert to N+1 with eager loading. With accessors, mutators, casting, and scopes, your code stays tidy and readable even as the application grows. Start with one simple model, then add relationships and scopes gradually as your feature needs increase.