What is Eloquent in Laravel 13: A Guide and CRUD Operations with the ORM

- Andrés Cruz - ES En español

Video thumbnail

Content Index

In Laravel, practically everything is articulated around object-oriented classes. The true power of models lies in the fact that they inherit from the base class Illuminate\Database\Eloquent\Model, which automatically grants them the ability to communicate with the database without the need to configure tedious manual mappings.

By making a class inherit from Model, we immediately get a wide range of built-in methods to interact with the tables. This design pattern is known as Active Record, where each model class represents a table in the database and each instance of that class corresponds to a specific row or record.

By convention, Laravel performs an automatic matching: if your model is named Post (singular and in PascalCase), the framework defaults to connecting to the posts table (plural and lowercase). Thanks to this convention-over-configuration magic, you don't need to define table names unless you use an unconventional structure; inheritance gives you all CRUD operations right out of the box (acronym for Create, Read, Update, Delete; translated as Crear, Leer, Actualizar y Eliminar).

In this article, we will explore the most common day-to-day operations, but the Eloquent ecosystem offers many additional functions that you can consult in the official documentation:

https://laravel.com/docs/master/eloquent

Previously we analyzed how to manage transactional batch operations in Laravel to ensure atomicity and data integrity. Now we will take a fundamental step by mastering essential Eloquent operations.

What is Eloquent and what is an ORM?

Video thumbnail

To manipulate data fluidly and expressively, Laravel uses Eloquent, which is an ORM (Object-Relational Mapping).

To understand its importance in modern web development, let's break down the three pillars that make up this concept:

  • Object: Because we work directly with strictly typed PHP classes, objects, methods, and properties.
  • Relational: Because our databases are relational (such as MySQL, MariaDB, PostgreSQL, SQLite, or SQL Server), organized in structured tables with primary keys (PK) and foreign keys (FK).
  • Mapping: Because Eloquent handles bidirectional "translation": it converts each row of your relational table into a live PHP object ready to manipulate, and translates every change in the object into SQL statements upon persistence.

Relational vs. Non-Relational Databases: Where does Eloquent fit?

A recurring doubt when getting into backend development is the fundamental distinction between a relational and a non-relational database:

  • Relational Databases (SQL / RDBMS): They organize information into rigid tables composed of columns with defined types and rows of data. Integrity is maintained through strict constraints and relationships between tables (one-to-one, one-to-many, many-to-many) with foreign keys. Direct examples are MySQL, MariaDB, PostgreSQL, and SQLite. Eloquent is built from the ground up to interact with this type of relational database.
  • Non-relational Databases (NoSQL): They store information in flexible formats like JSON documents (e.g., MongoDB), key-value pairs (like Redis), or graphs. They do not require a fixed tabular schema or classic table relationships.

In Laravel, the model represents the fundamental abstraction layer with the database engine (DB). This provides a huge architectural advantage: the application becomes independent of the underlying engine. You can develop locally with an in-memory or file-based sqlite, and switch to mysql or pgsql in production environments by simply adjusting the environment variables in your .env file, without having to rewrite a single query in your code.

1. Comparison: Pure PHP with SQL vs. Eloquent ORM in Laravel

In traditional PHP development without a framework (or using classic PDO), database interaction requires manually concatenating SQL text strings and managing bound parameters. For example, to get active products:

// Traditional approach with pure PHP and native SQL
$sql = "SELECT * FROM productos WHERE status = 'activo'";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$productos = $stmt->fetchAll(PDO::FETCH_ASSOC);

In Laravel, Eloquent completely abstracts this complexity through a Fluent Syntax tied to the Product model:

// Modern and expressive approach with Eloquent ORM
$productos = Product::where('status', 'activo')->get();

This abstraction not only makes the code much cleaner, more concise, and maintainable, but it also transparently implements statement preparation via PDO (parameter binding), radically neutralizing the most common SQL injection vulnerabilities.

2. Main advantages of using Eloquent ORM

  • Database engine portability: By not tying you to a specific SQL dialect, you can migrate engines without altering the business logic of your controllers and services.
  • Debugging and query traceability: Eloquent acts as a transparent translator. With methods like toSql() or tools like Laravel Debugbar, you can inspect with total precision the SQL statement that will be sent to the manager.
  • Table mapping via Models: Each table in the schema is reflected in a specific model (for example, the User model is associated with the users table, and the Post model with the posts table).
  • Elegant relationship management: Allows querying linked data between tables without writing complex JOIN clauses by hand.

3. Simplification of CRUD operations and relationships in Laravel

With Eloquent, creating records, manipulating fields, and applying transformations are handled using simple associative arrays:

// Safe mass insertion with Eloquent
$user = User::create([
    'name' => 'Juan',
    'email' => 'juan@example.com',
    'password' => bcrypt('secreto'),
]);

Table relationship management

Instead of writing manual JOIN statements and mapping duplicate columns, in Eloquent you declare relationships right inside the models (using methods like hasOne, hasMany, belongsTo, or belongsToMany):

// Fetching a user and loading their related posts
$user = User::find(1);
$posts = $user->posts; // hasMany relationship automatically resolved

4. Advanced data handling: Collections in Laravel

When you execute an Eloquent query that returns multiple rows (using, for example, get() or all()), the results are not returned in a simple native PHP array, but inside an instance of Illuminate\Database\Eloquent\Collection.

Laravel collections wrap data with dozens of higher-order utility methods to filter, transform, sort, or group information directly in memory in a chained manner:

// Direct filtering on the collection retrieved in memory
$productosCaros = $productos->filter(function (Product $producto) {
    return $producto->precio > 100;
});

Eloquent ORM turns relational rows into live objects with their own behavior, eradicating typical syntax errors when concatenating SQL strings. And when you face high-demand analytical scenarios where you require an extremely optimized query, Laravel always allows you to resort to the Query Builder or direct SQL statements (Raw SQL with DB::raw()) without giving up the framework's comfort.

CRUD Operations with Eloquent on Models

Next, we will analyze step-by-step how to implement the four basic operations of a CRUD on a representative model: Post.

1. Creating a record (Create / Eloquent Insert)

To insert a new record into the database in bulk, we can resort to the static method Post::create(), passing it an associative array with the key-value pairs we wish to save:

use App\Models\Post;

public function store(): Post
{
    return Post::create([
        'title' => 'Introducción a Eloquent',
        'slug' => 'introduccion-a-eloquent',
        'content' => 'Contenido detallado de la publicación...',
        'category_id' => 1,
        'description' => 'Breve resumen del post',
        'posted' => 'yes',
        'image' => 'portada.jpg',
    ]);
}

It is essential to keep Foreign Key Constraints in mind. If the category with id = 1 does not previously exist in the categories table, the database engine will stop the operation throwing a referential integrity violation error:

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`midatabase`.`posts`, CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`))

To avoid this, make sure the parent record exists beforehand. You can create it via a seeder, from tinker, or directly with the Category model:

use App\Models\Category;

Category::create([
    'title' => 'Desarrollo Web',
    'slug' => 'desarrollo-web',
]);

Mass Assignment and the $fillable property

If you execute the create() method for the first time on a new model, you will very likely encounter the following Laravel exception:

Illuminate\Database\Eloquent\MassAssignmentException: Add [title] to fillable property to allow mass assignment on [App\Models\Post].

This exception is not a random programming bug; it is a critical protection measure against Mass Assignment Vulnerabilities. When you receive data from an HTTP request (for example, with $request->all()) and pass it directly to the create() method, an attacker could manipulate the request by sending sensitive fields you didn't plan to modify, such as role = 'admin', is_verified = true, or alterations to balances and subscriptions.

To protect your application, Laravel requires you to explicitly define which table columns are authorized to be mass-filled. This is traditionally configured in the model via the protected property $fillable:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Attributes that can be assigned in bulk.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'title',
        'slug',
        'content',
        'category_id',
        'description',
        'posted',
        'image',
    ];
}

Once $fillable is defined, when you execute the insertion again, the record will be created successfully and Laravel will return the newly persisted instance along with its timestamps (created_at and updated_at) and its auto-generated id:

{
  "id": 1,
  "title": "Introducción a Eloquent",
  "slug": "introduccion-a-eloquent",
  "content": "Contenido detallado de la publicación...",
  "category_id": 1,
  "description": "Breve resumen del post",
  "posted": "yes",
  "image": "portada.jpg",
  "created_at": "2026-03-02T19:43:42.000000Z",
  "updated_at": "2026-03-02T19:43:42.000000Z"
}

Return types in controllers: Model vs. Response

If you are testing this code inside a web controller and type hint the return as Illuminate\Http\Response, but return the Post model directly:

use Illuminate\Http\Response;

public function index(): Response
{
    return Post::create([...]); // Type error!
}

You will get a PHP type exception:

App\Http\Controllers\PostController::index(): Return value must be of type Illuminate\Http\Response, App\Models\Post returned

Remember that in a web architecture, the controller processes a Request object and must return a valid HTTP response. If you are building a REST API, the usual practice is to transform the resource and return a structured JSON response with its 201 Created HTTP status code:

use Illuminate\Http\JsonResponse;

public function store(Request $request): JsonResponse
{
    $post = Post::create($request->validated());

    return response()->json($post, 201);
}

2. Updating a record (Update)

Unlike initial mass creation where we invoke a static method (Post::create()) because the entity does not yet exist in the database, to update we first retrieve the existing instance and then invoke the update() method:

public function update(int $id): bool
{
    $post = Post::findOrFail($id);

    return $post->update([
        'title' => 'Título actualizado de la publicación',
        'description' => 'Nueva descripción editada...',
    ]);
}

Route Model Binding

Laravel includes an extremely elegant mechanism called Route Model Binding. If in your route definition you declare a parameter that matches the model name:

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');

And then in your controller method you type hint the parameter with the corresponding model class (Post $post), Laravel will resolve the query for you in the background:

  • Without automatic injection: You receive a scalar identifier (int $id) and are forced to query manually with Post::findOrFail($id).
  • With Route Model Binding: Laravel automatically looks up the record by its primary key id. If the record exists, it injects the fully hydrated $post instance into your method; if it doesn't exist, it stops execution immediately and throws a 404 Not Found error.
// Clean controller thanks to dependency injection and Route Model Binding
public function edit(Post $post)
{
    return view('posts.edit', compact('post'));
}

public function update(Request $request, Post $post)
{
    $post->update($request->validated());

    return redirect()->route('posts.show', $post)->with('success', 'Post actualizado con éxito.');
}

3. Retrieving and querying records (Read / Eloquent Select)

To query information from the database, Eloquent offers versatile methods adapted to every need.

Getting all records: all() and get()

When you need to list all records from a table without prior filters, you can use all():

$categories = Category::all();

On the other hand, get() is used at the end of a chain of query conditions or restrictions:

$postsPublicados = Post::where('posted', 'yes')
    ->orderBy('created_at', 'desc')
    ->get();

Debugging tip with dd(): The helper function dd() (Dump and Die) is your essential ally in daily development. It prints the content of any collection, model, or variable with a collapsible format and colors, interrupting script execution so you can inspect the data before rendering the view.

$categories = Category::all();
dd($categories); // Shows the category collection and stops execution

Searching by primary key: find() and findOrFail()

To retrieve a single record based on its primary key (id), we use find():

$post = Post::find(1);
  • It accepts an integer or a string (if working with UUID identifiers).
  • If the record exists in the table, it returns the Post model instance; if it does not exist, it returns null.
  • If you need it to throw an automatic 404 Not Found error when the record is not found, use Post::findOrFail(1).

Getting the first result: first()

Unlike get() (which always returns a collection of models), the first() method executes a LIMIT 1 in the SQL query and exclusively returns the first matching model (or null):

$category = Category::where('slug', 'desarrollo-web')->first();

Custom queries with where()

The where() method allows you to apply filters with complete naturalness:

// Generated SQL: SELECT * FROM categories WHERE title = 'Tecnología' LIMIT 1;
$category = Category::where('title', 'Tecnología')->first();

// Also supports comparison operators
$postsRecientes = Post::where('id', '>', 10)->get();

4. Deleting a record (Delete)

To delete a record, it must previously exist in the database. In Eloquent, we simply retrieve the model instance and call its delete() method:

public function destroy(int $id): bool
{
    $post = Post::findOrFail($id);

    return $post->delete();
}

Taking advantage of Route Model Binding, the controller action is reduced to its bare minimum:

public function destroy(Post $post)
{
    $post->delete();

    return redirect()->route('posts.index')->with('status', 'Registro eliminado correctamente.');
}
  1. If the record exists: Laravel injects the $post object and executes physical deletion in the database (or soft deletion if you have the SoftDeletes trait implemented).
  2. If the record does not exist: Laravel aborts the request with a 404 error immediately, preventing any subsequent instructions from running.

Class Attributes: The New Syntax in Laravel 13

Starting from recent versions of Laravel and fully consolidated in Laravel 13, the framework leverages native PHP 8 Attributes to configure models in a cleaner, more compact, and modern way.

In default Laravel 13 models, you will notice that alongside classic protected properties we can define configuration directly on the class using bracket and hash attributes (#[...]).

How to implement the #[Fillable] Attribute

To migrate from the classic protected $fillable property to the new class attribute, we import the Illuminate\Database\Eloquent\Attributes\Fillable class and declare it right above the model definition:

app/Models/Category.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

#[Fillable(['title', 'slug'])]
class Category extends Model
{
    // It is no longer strictly mandatory to declare:
    // protected $fillable = ['title', 'slug'];

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

Which of the two syntaxes should you use?

Both syntaxes are fully compatible and work exactly the same way underneath:

  • Classic Syntax (protected $fillable): It is historically the most widespread. You will see it in the vast majority of packages, legacy projects, and existing tutorials.
  • PHP Attribute Syntax (#[Fillable]): It is more declarative, reduces boilerplate code, and leverages the language's native introspection without cluttering the class body with protected variables.

Advanced Operations in Eloquent and Query Builder

Both Eloquent and Laravel's underlying Query Builder (DB::table()) share a unified query interface. Knowing these methods will allow you to solve complex filtering and data extraction requirements with maximum efficiency.

To experiment with these queries in real time without bringing up views, I recommend using Laravel's interactive console:

php artisan tinker

Inspecting the SQL statement with toSql()

If you want to know the exact SQL statement that Laravel is going to execute, replace terminal methods like get() or first() with toSql():

// With the DB facade Query Builder:
$sql = DB::table('posts')->where('id', '>', 5)->toSql();
// Result: "select * from `posts` where `id` > ?"

// Directly from the Eloquent model:
$sql = Post::where('posted', 'yes')->orderBy('created_at', 'desc')->toSql();
// Result: "select * from `posts` where `posted` = ? order by `created_at` desc"

Joining tables with join()

When you need to combine fields from two relational tables based on a common key, you can apply the join() clause:

$posts = Post::join('categories', 'categories.id', '=', 'posts.category_id')
    ->select('posts.*', 'categories.title as category_title')
    ->orderBy('posts.created_at', 'desc')
    ->paginate(10);

Methods like select(), where(), and orderBy() can be chained fluently, always placing them before the terminal methods that resolve the query (such as get(), first(), or paginate()).

Logical grouping with nested where() and orWhere()

An extremely common mistake in SQL occurs when we combine AND and OR conditions at the same level without parentheses, causing unexpected records to be returned due to operator precedence.

To group conditions with parentheses in Eloquent, we pass an anonymous function (Closure) inside where():

$categorySlug = 'laravel';

$posts = Post::join('categories', 'categories.id', '=', 'posts.category_id')
    ->select('posts.*', 'categories.title as category_title', 'categories.slug as category_slug')
    ->where('categories.slug', $categorySlug)
    ->where('posted', 'yes')
    ->where(function ($query) {
        $query->where('type', 'post')
            ->orWhere('type', 'course')
            ->orWhere('type', 'tutorial');
    })
    ->orderBy('posts.created_at', 'desc')
    ->paginate(10);

If you need to pass external variables inside the closure, use the use ($variable) directive:

$categoryId = 1;

$posts = Post::where('id', '>=', 1)
    ->where(function ($query) use ($categoryId) {
        $query->where('category_id', $categoryId)
            ->orWhere('posted', 'yes');
    })
    ->get();

Filtering by sets with whereIn() and whereNotIn()

To search for records whose column matches any of the values present in an array:

$ids = [1, 2, 3, 4, 5];

$postsIncluidos = Post::whereIn('id', $ids)->get();
$postsExcluidos = Post::whereNotIn('id', $ids)->get();

Manual pagination with limit() and offset() (or take() and skip())

If you do not want to use Laravel's automatic paginator (paginate()) and prefer to limit the number of results or skip a batch of records:

// Limit to 5 records skipping the first 10:
$posts = Post::offset(10)->limit(5)->get();

// Or with its equivalent Eloquent syntax:
$posts = Post::skip(10)->take(5)->get();

Counting and random ordering

// Count the total rows matching the condition directly in SQL:
$totalActivos = Post::where('posted', 'yes')->count();

// Get records in random order:
$destacados = Post::where('posted', 'yes')->inRandomOrder()->limit(3)->get();

Serialization to JSON and Array

Serialization is the process of converting Eloquent objects into flat formats to be transmitted via the web, essential when building REST APIs:

$post = Post::find(1);

// Convert to PHP associative array:
$array = $post->toArray();

// Convert to JSON formatted text string:
$json = $post->toJson();

Cloning Models with replicate()

Video thumbnail

On many occasions we need to take an existing database record, duplicate it with minor variations, and persist it as a completely new row. To solve this flawlessly, Laravel provides us with the replicate() method.

// Duplicate a shipment modifying only the type
$shipping = Shipping::findOrFail(1);
$newShipping = $shipping->replicate();

$newShipping->fill([
    'type' => 'billing',
]);

$newShipping->save();

The replicate() method also accepts an array with the list of columns you want to explicitly exclude so they are not copied into the new record:

$newShipping = $shipping->replicate([
    'tracking_number',
    'shipped_at',
]);

$newShipping->type = 'express';
$newShipping->save();

Practical case in a payment and course workflow (Legacy Version)

Let's look at a real example extracted from an enrollment module: when a student acquires a new course that has a previous version (Legacy), we replicate the payment record to grant them access to both versions:

// Creation of the main course payment
$payment = CoursePayment::create([
    'user_id' => $user->id,
    'purchasable_id' => $newCourse->id,
    'purchasable_type' => Course::class,
    'amount' => $amount,
    'reference' => $reference,
]);

// If the purchased course has an older version (Legacy)
if ($newCourse->has_legacy_version) {
    // We replicate the original payment instance maintaining integrity
    $legacyPayment = $payment->replicate();

    // We specifically modify the required fields
    $legacyPayment->purchasable_id = $newCourse->legacy_id;
    $legacyPayment->reference = $payment->reference . '-LEGACY';

    // We save the new cloned record
    $legacyPayment->save();
}

Manually modifying properties on the cloned instance brings great expressiveness to the code, making it crystal clear which attributes need to be differentiated before persisting with save().

pluck() vs. modelKeys(): How to get an array of IDs in Eloquent

A common requirement in Laravel consists of extracting a simple array with the IDs of a set of related records (for example, the permissions associated with a role). The decision between pluck() and modelKeys() has direct implications on your application's performance.

1. Using pluck()

The pluck() method extracts the values of a specific column and returns a collection with those values:

// At the SQL query level (SELECT id FROM permissions...):
$permissionIds = Permission::where('active', true)->pluck('id');

// On a relation already loaded in memory:
$permissionIds = $role->permissions->pluck('id');

Additionally, pluck() supports using anonymous functions (closures) to generate custom key-value pairs (for example, to populate <select> elements in Blade views):

$options = Product::available()->get()->pluck(
    fn (Product $p) => "{$p->brand} - {$p->model}", 
    'id'
);

2. Using modelKeys()

The modelKeys() method belongs exclusively to Eloquent collections (Illuminate\Database\Eloquent\Collection) and returns a flat array with the primary keys of the contained models:

// Returns the array of primary keys:
$ids = $role->permissions->modelKeys();

The advantage of modelKeys() is that it automatically respects custom primary keys (defined in the model's $primaryKey property). However, it does not exist in the query builder or directly on the model. If you execute Permission::modelKeys() you will get an error.

Golden rule on performance: Never execute Model::all()->modelKeys() just to get a list of identifiers. That would force Laravel to query and instantiate thousands of complete models with all their columns in memory. Instead, always execute Model::pluck('id'), which will generate a lightweight SQL query that fetches only the required column directly from the database engine.

Common error: Why only() does not work on model attributes inside a collection

A recurring confusion among developers is attempting to filter model attributes using only() on the collection:

// This will return an empty collection or not filter the model's fields!
$collection->only(['title', 'slug']);

The reason is that the collections' only() method operates on the internal keys of the collection itself, not on the properties or columns of the Eloquent model. If you want to limit the retrieved columns, indicate it directly in the SQL query with select('title', 'slug').

Retrieving the Last Inserted ID in Laravel

After inserting a record into the database, we frequently need its freshly generated identifier to link child records or redirect the user. We have several methods to achieve this:

1. Via Eloquent's create() method (Recommended)

The create() method immediately returns the newly persisted model instance, with its ID available:

$post = Post::create([
    'title' => 'Nuevo Post',
    'slug' => 'nuevo-post',
]);

$nuevoId = $post->id;

2. Via the save() method

When instantiating a model manually and calling save(), Eloquent automatically updates the instance's id property:

$post = new Post();
$post->title = 'Nuevo Post';
$post->slug = 'nuevo-post';
$post->save();

$nuevoId = $post->id;

3. With the Query Builder via insertGetId()

If you use the DB facade instead of models, the insertGetId() method inserts the row and returns the identifier in a single operation:

$id = DB::table('posts')->insertGetId([
    'title' => 'Post vía Query Builder',
    'slug' => 'post-via-query-builder',
]);

4. Via latest('id')->first()

$ultimoPost = Post::latest('id')->first();
$ultimoId = $ultimoPost?->id;

5. Via PDO with lastInsertId()

If you execute raw queries directly on PDO, you can check the connection's last identifier:

DB::table('posts')->insert([
    'title' => 'Post con PDO crudo',
    'slug' => 'post-pdo-crudo',
]);

$id = DB::getPdo()->lastInsertId();

Model Casts Methods: Attribute Conversion

Attribute casting in Eloquent is an indispensable functionality to convert primitive database data types into native PHP types (such as integers, booleans, collections, Carbon dates, or backed Enums).

Starting from Laravel 11 and fully current in Laravel 13, the recommended way to define casts is through the casts() method, overcoming the historical limitation of the protected $casts property which did not support invoking static methods:

namespace App\Models;

use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Define the model's attribute casts.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'is_active' => 'boolean',
            'role' => UserRole::class,
        ];
    }
}

In Laravel 10, this same conversion with Enums required concatenating text strings in the $casts property:

// Laravel 10 legacy approach:
protected $casts = [
    'options' => AsEnumCollection::class . ':' . UserOption::class,
];

The new casts() method provides type safety, IDE autocompletion, and the ability to chain static methods like AsEnumCollection::of(UserOption::class).

Dynamic Filters with Conditional Clauses: when() and whereBetween()

Video thumbnail

When building administration panels (whether in Livewire, Vue, or classic Blade controllers), it is very common to have optional search, status, category, and date range filters.

Instead of nesting multiple traditional if blocks that fragment the query, the recommended way in Laravel is to use the when() method:

use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;

$posts = Post::query()
    ->when(request('search'), function (Builder $query, string $search) {
        $query->where(function ($q) use ($search) {
            $q->where('id', 'like', "%{$search}%")
                ->orWhere('title', 'like', "%{$search}%")
                ->orWhere('description', 'like', "%{$search}%");
        });
    })
    ->when(request('category_id'), function (Builder $query, int $categoryId) {
        $query->where('category_id', $categoryId);
    })
    ->when(request('type'), function (Builder $query, string $type) {
        $query->where('type', $type);
    })
    ->when(request('posted'), function (Builder $query, string $posted) {
        $query->where('posted', $posted);
    })
    ->when(request('from') && request('to'), function (Builder $query) {
        $query->whereBetween('created_at', [request('from'), request('to')]);
    })
    ->with('category')
    ->orderBy('created_at', 'desc')
    ->paginate(15);

The when() method evaluates the first parameter as a boolean: if it has a truthy value (non-null and non-empty), it executes the closure passing the evaluated value; if it is false or null, it skips the block and continues the chain without altering the query.

Implementation in the interface with Laravel Livewire

In a Blade view with Livewire, these reactive filters can be structured intuitively:

<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
    <select class="block w-full rounded border-gray-300" wire:model.live="posted">
        <option value="">{{ __('All statuses') }}</option>
        <option value="not">{{ __('Draft') }}</option>
        <option value="yes">{{ __('Published') }}</option>
    </select>

    <select class="block w-full rounded border-gray-300" wire:model.live="category_id">
        <option value="">{{ __('All categories') }}</option>
        @foreach ($categories as $id => $title)
            <option value="{{ $id }}">{{ $title }}</option>
        @endforeach
    </select>

    <input type="text" class="block w-full rounded border-gray-300" wire:model.live.debounce.300ms="search" placeholder="{{ __('Search by title...') }}" />

    <div class="grid grid-cols-2 gap-2">
        <input type="date" class="block w-full rounded border-gray-300" wire:model.live="from" />
        <input type="date" class="block w-full rounded border-gray-300" wire:model.live="to" />
    </div>
</div>

You can check the complete component code in the example repository:

https://github.com/libredesarrollo/book-course-laravel-livewire/blob/main/resources/views/livewire/dashboard/post/index.blade.php

Filtering by date ranges with whereBetween()

Video thumbnail

To filter records between two extreme dates (initial and final bounds), the whereBetween() method receives the column name and an array with the two limit values:

use Carbon\Carbon;

if (request()->filled(['from', 'to'])) {
    $posts->whereBetween('created_at', [
        Carbon::parse(request('from'))->startOfDay(),
        Carbon::parse(request('to'))->endOfDay(),
    ]);
}

And if you are working on an SPA application with Vue 3 / Inertia:

resources/js/Pages/Dashboard/Post/Index.vue

<template>
  <div class="flex gap-4 mb-4">
    <TextInput type="date" placeholder="From Date" v-model="from" />
    <TextInput type="date" placeholder="To Date" v-model="to" />
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import { router } from '@inertiajs/vue3';

const from = ref('');
const to = ref('');

watch([from, to], () => {
  if (from.value && to.value) {
    router.get(route('posts.index'), { from: from.value, to: to.value }, { preserveState: true });
  }
});
</script>

Viewing and Debugging SQL Queries in Laravel: N+1 Problem Detection

Writing queries with Eloquent is extraordinarily convenient, but we must never forget that underneath, SQL statements are executed in a relational database engine. If we do not monitor what queries are being fired, it is very easy to introduce severe performance bottlenecks.

The danger of the N+1 problem and how to solve it with with() (Eager Loading)

The most widespread performance error in Laravel is the N+1 problem. It occurs when we load a list of records and then, inside a loop in the view or controller, access an unloaded relationship:

// Inefficient code (Triggers N+1 queries):
$posts = Post::limit(20)->get(); // 1 query

foreach ($posts as $post) {
    echo $post->category->title; // Triggers 1 additional query for EACH post! (20 queries)
}
// Total: 1 + 20 = 21 SQL queries

To solve this at the root, we use eager loading via the with() method:

// Optimized code with Eager Loading:
$posts = Post::with('category')->limit(20)->get();

foreach ($posts as $post) {
    echo $post->category->title; // The relationship is already loaded in memory
}
// Total: Only 2 SQL queries in total (one for posts and another with WHERE IN for categories)

Tools and methods to inspect executed SQL

1. Laravel Debugbar (Recommended in development)

It is the community standard for inspecting queries in local environments. It shows the exhaustive list of all SQL statements executed in the request, bound parameters, time in milliseconds, and automatic alerts for duplicate queries:

composer require barryvdh/laravel-debugbar --dev

2. Laravel Telescope and Laravel Pulse

For comprehensive real-time monitoring, Laravel Telescope allows auditing requests, exceptions, queued jobs, and detailed queries:

composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

3. Query event logging with DB::listen()

You can listen to each executed query by registering a listener in the boot() method of your AppServiceProvider to send statements to Laravel logs:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

public function boot(): void
{
    if (app()->isLocal()) {
        DB::listen(function ($query) {
            Log::info("SQL: {$query->sql} [Time: {$query->time}ms]", [
                'bindings' => $query->bindings,
            ]);
        });
    }
}

Performance warning: Never keep DB::listen() active without filters in production environments, as it increases I/O overhead and can generate excessive log files.

4. Activate spot logging with DB::getQueryLog()

Ideal for isolated scripts, artisan commands, and unit tests with Pest:

DB::enableQueryLog();

// Execute your queries with Eloquent
$posts = Post::where('posted', 'yes')->with('category')->get();

$queries = DB::getQueryLog();
dd($queries); // Shows the array with statements executed during the block

5. Interactive inspection with Tinkerwell

For spot debugging without raising the browser or executing complete views, tools like Tinkerwell allow writing queries with Eloquent or the Query Builder and immediately visualizing the result and generated SQL.

Architecture in Laravel: From Fat Models to Services

Video thumbnail

As applications grow, it is very common to fall into the Fat Model antipattern: accumulating business validations, payment processing, subscriptions, notifications, and coupon calculations inside the model class.

When a model exceeds hundreds of lines mixing persistence responsibilities with complex business logic, maintenance and automated testing become a critical challenge.

Segregation of responsibilities with the Service Layer

The optimal architectural solution consists of delegating business logic to Service Classes, keeping models strictly focused on data representation, scopes, and relationships:

namespace App\Services;

use App\Models\Tutorial;
use App\Models\User;

class TutorialService
{
    /**
     * Determine if the resource is accessible to the user.
     */
    public function isAccessible(User $user, Tutorial $tutorial, ?string $coupon = null): bool
    {
        if ($tutorial->price === 0) {
            return true;
        }

        return $user->hasPurchased($tutorial) || $this->applyCoupon($tutorial, $coupon);
    }
}

The Service Container and Dependency Injection

Laravel includes a powerful Service Container that automatically resolves dependencies via class reflection. You can inject your services directly into your controllers' constructors or methods:

$tutorialService = app(TutorialService::class);

Frequently Asked Questions (FAQ) about Eloquent ORM in Laravel

  • What is Eloquent in Laravel?
    • Eloquent is the official ORM (Object-Relational Mapping) included by default in the Laravel framework. It implements the Active Record pattern, allowing the manipulation of relational database tables via PHP classes and objects.
  • What is the difference between all() and get()?
    • all() is a direct static method that returns all table rows without filters. Conversely, get() is chained at the end of queries built with clauses like where(), orderBy(), or with().
  • How to solve the N+1 query problem?
    • It is solved by using Eager Loading via the with('relation') method before executing the query, preventing an individual query from firing for each element in a loop.
  • Why use #[Fillable] instead of protected $fillable?
    • The PHP attribute syntax introduced in Laravel 13 allows a cleaner and more modern declaration at the class metadata level, although both ways are fully compatible.
  • When to use pluck() vs. modelKeys()?
    • Use pluck('id') at the SQL query level to extract only that column without overloading memory. Use modelKeys() only if you already have the model collection loaded previously in memory.

Conclusion and Next Steps

Mastering CRUD operations on models and deeply understanding how Eloquent ORM works is the fundamental pillar for any developer aspiring to build professional, secure, and scalable applications with Laravel.

From proper mass assignment configuration with $fillable or Laravel 13 native attributes, through building efficient queries with when() and whereBetween(), to detecting N+1 queries with tools like Debugbar or Telescope, every detail influences the speed and quality of your software.

Now that you master data manipulation with Eloquent models, the natural next step consists of exposing this logic to the outside world. I invite you to continue expanding your skills by creating a complete REST API in Laravel.

Discover what Eloquent ORM is in the Laravel 13 framework. Learn how to perform full CRUD operations using models, #[Fillable] attributes, filters, and optimized queries.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.