CRUD Operations on Models with the Eloquent ORM in Laravel 13 - The Definitive Guide

Video thumbnail

Content Index

In Laravel, everything is a class. The importance of models lies in the fact that they inherit from the Model class, which automatically gives them the ability to interact with the database.

Ultimately, it's about taking our model, which inherits from Model, meaning it's a model class, and with it, we get multiple free functions to communicate with the database, like the ones we saw earlier.

By convention, Laravel automatically makes a "match": if your model is named Post (singular), it understands that it should connect to the posts table (plural). You don't need to configure anything else; the magic of inheritance already gives you methods to create, read, update, and delete (CRUD) for free.

It's important to mention that these are only some of the operations (the ones we will see), the most common ones, but we have many more:

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

What are Eloquent and ORM?

To interact with data, Laravel uses Eloquent. This is an ORM (Object-Relational Mapping).

  • Object: Because we work with PHP classes and objects.
  • Relational: Because our databases are relational (like SQLite or MySQL).
  • Mapping: Because Eloquent "maps" or translates each row of your table into a PHP object.

So, instead of writing complex SQL, you simply use methods like Post::all() to get everything or Post::create() to insert. The names in Laravel (Eloquent, Blade, Artisan) sound almost like superheroes, and they really are for our productivity!

Also remember that models are the only layer we have to communicate with the database; which makes it excellent, since with this we can make the database we are working with, for example, MySQL, MariaDB, PSQL, SQL Server... independent of the project; we can change from one database to another easily and with few configurations.

In this section, we will look at CRUD operations on the database using models.

CRUD operations with Eloquent on models

Let's see how to perform CRUD operations on models, in this case, a Post model.

Create a record

Finally, if we want to create a post:

public function index()
    {
        return Post::create(
            ['title' => "test",
             'slug' => "test",
             'content' => "test",
             'category_id' => 1,
             'description' => "test",
             'posted' => "not",
             'image' => "test"]
        );
}

It is important that the category with ID 1 exists; otherwise, you will see an error like the following:

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

You can create the category from the database.

If we go to the browser:

  • http://larafirststeps.test/post

You will see that an error occurs:

Add [title] to fillable property to allow mass assignment on [App\Models\Post].

At first, the error may seem strange, but what is happening is that an extra configuration is missing on our model; we must tell Laravel which columns can be managed for our posts; that is, created or updated; this is particularly useful since you can have columns that you do not want to be manipulated by the framework, which handle sensitive information, for debugging, control, or security.

Suppose you have a user model with a role column, whose role can be either administrator or regular, there is only one administrator in the application, which is the one you create directly in the database; therefore, you will not want the roles column to be managed by the framework, and this is to avoid possible vulnerabilities in which a malicious user could exploit a vulnerability in the application in which a user is created or managed and could change the user's role by that means; this, to give an example.

Returning to the manageable fields or columns, to define them we have to define a protected property called fillable in the database, which is nothing more than an array that defines the fields that are "fillable":

protected $fillable = ['title', 'slug', 'content', 'category_id', 'description', 'posted', 'image'];

With this, if we enter the page again:

{
  "title": "test",
  "slug": "test",
  "content": "test",
  "category_id": 1,
  "description": "test",
  "posted": "not",
  "image": "test",
  "updated_at": "2026-03-02T19:43:42.000000Z",
  "created_at": "2026-03-02T19:43:42.000000Z",
  "id": 1
}

We will see that it was created correctly; from the browser, we will see an error like the following:

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

And this is because of the type that we must return; following the client/server scheme, the client, from the browser, makes a query to our application, where the query is processed by a controller; the query is the request; for example:

use Illuminate\Http\Request;
public function store(Request $request): {
}

Now, if we try to create the post, we will see an error due to the FK:

SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed (Connection: sqlite, Database:

And it is that, first we must create the category and create the fillable fields as we did with the posts:

Category::create([
   'title' => 'Cate 1',
   'slug' => 'cate-1'
]);

The next step is for the server to return a response, which is done from the same controller that receives the request; this response, in Laravel, is of the type:

use Illuminate\Http\Response;
class PostController extends Controller
{
    public function index(): Response
    {
    }
    ***
}

Which is different from the operation we are returning, which, in the previous example, is a post; and that is why the exception.

Update a record

Unlike creation, where we use a static method (Post::create) because the object does not exist yet, to update we work on an already existing instance.

To update a post, just use the update() method on the post we want to edit and indicate the fields:

public function index()
{
        $post = Post::find(1);
        return $post->update(
            [
                'title' => "test new",
                'slug' => "test",
                'content' => "test",
                'category_id' => 1,
                'description' => "test",
                'posted' => "not",
                'image' => "test"
            ]
        );
}

If we check the database, you will see that the record was updated.

If you want to practice with routes, for passing parameters in the route using square brackets []:

Route::get('/post/{post}/edit', [PostController::class, 'edit']);

Model Injection (Route Model Binding)

Laravel is incredibly smart: if in your controller you type-hint the parameter with the model name, the framework will do the dirty work for you.

  • Without injection: You would receive a number and would have to do Post::findOrFail($id).
    • public function edit(int $post)
  • With injection: Laravel looks for the record automatically. If the ID does not exist, it returns a 404 error immediately; if it exists, it gives you the $post object ready to use.
    • public function edit(Post $post)

Get records

Let's look at the most common methods we can use to get data, whether it's a collection or array or a single instance.

get() and all()

To get all records:

public function index()
{
    return Post::get();
}

To list everything in a table, the most direct method is all(), although we also tend to use get() when we apply previous filters.

$categories = Category::all();
dd($categories); // Dump and Die: Inspects the data and stops execution

The dd() method is your best friend. It's like a print or echo, but on steroids: it shows the data with colors, allows collapsing objects, and stops the code so that subsequent errors do not occur (like divisions by zero).

In this case, so that you can appreciate the returned format, the recommendation is that you have at least two posts in the database; just run the code we defined in the create a post section a couple more times:

[
  {
    "id": 1,
    "title": "test new",
    "slug": "test",
     ****
    "created_at": "2026-02-26T21:35:59.000000Z",
    "updated_at": "2026-03-02T19:47:18.000000Z",
    "category_id": 1
  },
  {
    "id": 2,
    "title": "test",
    "slug": "test",
    "created_at": "2026-02-23T15:19:35.000000Z",
    "updated_at": "2026-02-26T18:11:42.000000Z",
    "category_id": 1
  },
  {
 ****
]

To get a single post, we use the find() method which receives the id of the element we want to find:

public function index()
{
    return Post::find(1);
}

It is the short way to search for a record by its Primary Key (ID).

  • It accepts an integer or a string (in case you use UUIDs).
  • If the ID exists, it returns the object; if not, it returns null.

first()

Unlike get(), which returns a collection (array), first() returns only the first object that matches the query. It is ideal when you know you only need one result.

Custom queries with where()

What if you don't want to search by ID, but by title or slug? This is where we use the where() clause. Eloquent will automatically translate this to SQL.

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

The ORM Mapping

This is where the term ORM (Object-Relational Mapping) makes perfect sense:

  • Mapping: Laravel translates a row from your table (Relational) into an instance of a PHP class (Object).
  • Singular vs Plural: That's why the model is called Category (singular), because each object represents a row. When you get many, Laravel gives you an "Array" or Collection of those singular objects.

Delete a record

To delete something, just like to update it, it must first exist. It is a matter of logic applied to the real world: you cannot increase the memory of a phone you do not have, nor can you throw away a cake you have not baked. If you want the cake, you create it; if you eat a piece, you update it (modify its state); and if it goes bad, you delete it.

In Laravel, this relationship with the real world is maintained. To delete a record, the framework performs an implicit find using the Primary Key (PK). If the object exists, we proceed to delete it; if not, there is nothing to destroy.

Finally, to delete a post, given the post, we delete it with the delete() method.

public function index()
{
   $post = Post::find(1);
   return $post->delete();
}

Consistency in names is important. If in your route you define the parameter as {post}, in your method you must receive it as $post. Laravel uses Route Model Binding to find the record automatically:

public function destroy(Post $post)
  1. If the record exists: Laravel injects the object into the method and continues execution.
  2. If the record does not exist: Laravel returns a 404 error immediately and does not even execute the code inside the method (that's why, if you had a programming error like a division by zero, it would not explode if the ID is invalid).

Class Attributes: The New Syntax from Laravel 13

If you look closely at the default User model, you will notice that it no longer exclusively uses traditional properties like protected $fillable. Instead, a syntax based on PHP Attributes (those #[...] brackets above the class) appears.

Starting with Laravel 13, the framework allows defining model configuration as class attributes. This is a modern alternative to the classic way, and you can decide which one to use depending on your comfort level.

How to implement the Fillable Attribute

If you want to migrate from the classic way to this new syntax, you must make sure to import the correct namespace, as it is a common mistake to confuse it with other similar ones.

Implementation example:

First, import the attribute and then declare it on the class:

app/Models/Category.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['title', 'slug'])]
class Category extends Model
{
    // protected $fillable = ['title', 'slug'];
    function posts() {
        return $this->hasMany(Post::class);
    }
}

Which one should I use?

Both ways work exactly the same under the hood. However, here is a peer tip:

  • Classic Syntax (protected $fillable): This is what you will find in 99% of the current documentation and tutorials on the internet. If you use AI tools to help you, they will probably respond with this format because it is the historical standard.
  • Attribute Syntax: It is visually cleaner and follows the latest PHP trends.

Verifying the functionality

To confirm that this new syntax works, we can force an error. If we try to create a record omitting a mandatory field (like the slug) that we have defined in the attribute, Laravel will throw the same integrity violation exception that we saw before.

This shows that the attribute is fulfilling its function of "filtering" which fields can be mass-inserted.

Common operations in Eloquent (ORM)

These are some of the query builders that I have used in my projects in Laravel, and I want to bring some so that you have a slightly more complete reference of what you can do; again the purpose of these query builders, is that you can know how to perform operations that at some point you will need and their purpose in the next chapter is purely referential.

As a recommendation, try these queries, see the SQL generated by the toSql() method that we are about to evaluate.

In Laravel, the ORM we use is called Eloquent and is linked to the models, for example:

Post::where('posts.id',$id);

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

But, we can also use queries without using models, using the Query Builder:

DB::table('posts')->where('posts.id',$id);

Both schemes allow using most of the methods and are equivalent for most of the examples that we will see in this section:

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

For these exercises, you can use Tinker to test the queries from the command line:

$ php artisan tinker

View the SQL

If you want to see the generated SQL of a query, regardless of how complex it is, instead of indicating the  get(), find(), first() or any other method to get the data, replace it with the method of:

DB::table('posts')->toSql();
// "select * from "posts""
DB::table('posts')->where('id','>',5)->toSql()
// "select * from "posts" where "id" > ?"

Or in Eloquent:

$post = Post::where('id','>',5)->toSql()
// "select * from "posts" where "id" > ?"

Joins

Joins are a very useful structure with which we can combine different fields referenced by a common field; usually the foreign key:

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

In the previous example, you can see that, for demonstration purposes, other methods such as select() or orderBy() are used; since, usually to perform queries, several of these methods are used depending on what we want to do, we can usually place these methods anywhere in the query (except if we are grouping) but they must be before the methods that resolve the result, that is, before get(), find() or similar.

Remember that we have different types of joins, and in Laravel, we can use each of them:

https://laravel.com/docs/master/queries#joins

Ordering

If you do not want to order by id, which is the default value, you can order by a particular column:

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

Nested Where or orWhere

In Laravel, we have all types of where; but, we know that when we use a where and an orWhere at the same level without grouping them, what is returned is not what we expect; to be able to group using an orWhere or similar:

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

If we wanted to pass parameters, you can use the use method as you can see in the following example:

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

WhereIn and WhereNotIn

We also have the method to search by an array of ids:

$ids = array( 1, 2, 3, 4, 5, 6 );
$posts = Post::whereIn('posts.id',$ids);
$posts = Post::whereNotIn('posts.id',$ids);

Get a record

Regardless of the query, if you only want to get a single record in the result:

$posts = Post::where('slug', $slug)->first();

Limit the number of records

If you want to limit the number of records:

$posts = Post::limit(3)->get();

We can also indicate a specific block or page:

$posts = Post::limit(3)->offset(2)->get()    

Both functions are useful for creating custom pagination.

Count

To get the number of records, we can use the count() method:

Post::limit(2)->offset(2)->get()->count()  

Get random records

If you want to get records randomly:

$posts = Post::where(<>)->inRandomOrder()->get();

Serialization

Serialization refers to the process of converting objects or data structures into a particular format to be more easily transmitted and thus consumed, the JSON format is a good example of this, which is the format par excellence used when creating a Rest Api as we will see later.

Using Eloquent, we can convert the data obtained from a query to JSON format:

$post = Post::find(1);
$json = $post->toJson();

Or array:

$post = Post::find(1);
$array = $post->toArray();

Query constraints

With Eloquent, we can perform all kinds of constraints that accompany the query and thus be able to retrieve specific data, among the main ones we have:

  • "where": adds a basic where clause to the query.
  • "orWhere": adds an "or" where clause to the query.
  • "whereIn": adds a where in clause to the query.
  • "whereBetween": adds a where between clause to the query.
  • "orderBy": sorts the query results by a specified column.
  • "limit": limits the number of records returned by the query.
  • "offset": Specifies an OFFSET from where it starts returning data.

Limit and Offset

You can use the skip and take methods to limit the number of results returned by the query or to skip a certain number of results in the query:

$posts = Post::skip(10)->take(5)->get();

Alternatively, you can use the limit and offset methods. These methods are functionally equivalent to the take and skip methods, respectively:

$posts = Post::offset(10)->limit(5)->get();

All these constraints can be used together as we have seen in the previous examples:

$users = Post::where('type', 'post')
 ->orWhere('type', 'book')
 ->orderBy('created_at', 'desc')
 ->limit(10)
 ->get ();

pluck() vs modelKeys(): how to get an array of IDs from an Eloquent collection

There are many situations in Laravel where you simply need an array of IDs from a set of models. Nothing fancy: no extra fields, no transformations… just the identifiers.

It sounds simple, but once you start working with Eloquent relationships, non-standard primary keys, or large tables, the decision between pluck() and modelKeys() actually matters more than it seems.

I have run into this same problem many times, so let's break it down properly.

The common problem: getting an array of IDs in Laravel

A very typical scenario looks like this:

  • You have a hasMany relationship
  • An already loaded model
  • You need the IDs of the related models

For example, a Role that has many Permission models.

At that point, you usually already have an Eloquent Collection, not a query builder — and that detail is key.

Working with Eloquent relationships (hasMany example)

$permissionIDs = $role->permissions->pluck('id'); 

This works, it's readable, and most Laravel developers use it by instinct. And honestly, in real projects, this is still what I use most of the time.

But there's a trick

Using pluck() to extract IDs from a Collection

Basic usage of pluck('id')

pluck() extracts a given attribute from each model in the collection and returns a new collection containing those values.

$permissionIDs = $role->permissions->pluck('id'); 

You can then call ->toArray() if you really need a flat array.

When pluck() is the safest option

From experience, pluck() is the safest default option when:

  • You want a specific column
  • You might change the key name later
  • You are not 100% sure how the collection was built

It works on:

  • Query builders
  • Eloquent models
  • Eloquent collections

That flexibility alone makes it hard to beat.

Formatting values with closures in pluck()

This is where pluck() really shines, especially after support for closures was introduced.

For example:

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

No extra map(). No mapWithKeys(). No intermediate transformations.

I have used this a lot in Blade views for dropdown menus. It keeps the intention clear and the code chainable.

Using modelKeys(): what it really does

At first glance, modelKeys() looks like a clever shortcut:

$permissionIDs = $role->permissions->modelKeys(); 

Same result. Same number of characters. A little more "cool"

But what is it really doing?

How modelKeys() works internally

modelKeys() simply returns:

“The array of primary keys of the models in the collection.”

That's it.

No transformations. No formatting. No flexibility.

Primary keys and non-standard key names

An advantage of modelKeys() is that it respects custom primary keys.

If your model uses something other than id, modelKeys() will still work correctly — while pluck('id') would obviously fail unless you change the column name.

That is usually the first reason developers notice this method.

Why modelKeys() only works on Collections

This part confuses people all the time.

❌ This will fail:

Permission::modelKeys(); 

Because modelKeys() does not exist on the model or the query builder.

✅ This works:

Permission::all()->modelKeys(); 

But here's the catch — and this is important in real projects.

Loading all records into memory just to extract the IDs can be a serious performance problem on large tables. I have seen this done accidentally more than once.

pluck() vs modelKeys(): key differences

Flexibility vs simplicity

  • pluck() → flexible, expressive, works everywhere
  • modelKeys() → simple, strict, only for collections

If you need anything beyond “just give me the primary keys”, pluck() wins immediately.

Performance considerations

In practice:

  • pluck('id') can be executed at the query level (database)
  • modelKeys() requires an already loaded collection

That alone makes pluck() the best option when performance matters.

Common errors and misconceptions

A common misunderstanding is to think that modelKeys() is somehow “more efficient” because it sounds lower level.

It is not.

If you already have the collection loaded, sure — it's fine.

If you don't, forcing a full all() just to call modelKeys() is usually a bad idea.

Common errors when extracting values from Eloquent models

Why only() does not work on model attributes

This comes up often (and there is a classic Laracasts thread about it).

only() works on the collection keys, not on the model attributes.

So this will return an empty collection:

 
$options->only('responses'); 

Because responses lives inside each model, not at the collection level.

Collection vs Model: the mental model

This is the key mental shift that resolves most confusions:

  • Collections contain models
  • Collection methods do not magically access model attributes

Once you internalize that, pluck() suddenly makes perfect sense — and methods like only() stop being tempting in the wrong places.

Which one should you use in real projects?

Clear decision rules

Use pluck() when:

  • You do not have an already loaded collection
  • You need flexibility or formatting
  • Performance is important
  • You want predictable behavior everywhere

Use modelKeys() when:

  • You already have a loaded collection
  • You are only interested in the primary keys
  • The primary key name may vary

What I really use most of the time

In real-world Laravel applications, I still use pluck('id') by default.

It is useful to know modelKeys() — and I'm glad it exists — but it's more of a “nice to know” method than something I use daily.

Frequently Asked Questions

Is modelKeys() faster than pluck()?
No. If anything, pluck() is usually more efficient because it can be executed at the query level.

Can pluck() return formatted values?
Yes. With closures, it is extremely powerful and replaces many use cases of map().

Does modelKeys() work on Eloquent models?
No. Only on Eloquent collections.

Can I get IDs without loading the full models?
Yes — use pluck() directly on the query.

Conclusion

Both pluck() and modelKeys() solve the same problem on a superficial level, but they live in different layers of Eloquent.

If you understand when you are dealing with a collection versus a query, the right choice becomes obvious almost always.

Know modelKeys().
Use pluck().

Model Casts Methods from Laravel 11

Model casting in Laravel is a powerful tool for  transforming attribute values into other values, for example, we can convert texts to dates or numbers if necessary, and thus take advantage of all the functions provided by the Laravel ecosystem such as Carbon in the case of dates.

In Laravel, it is possible to cast to primitive types such as int, float, boolean, string, among others such as dates, which being of type Datetime can automatically be converted to Carbon, which is a library used by Laravel to facilitate the manipulation of dates.

Model casts in Laravel 10 are defined using the $casts array property. However, in Laravel 11, you can define a casts() method, which opens up the possibility of using static methods in a custom way:

use App\Enums\UserOption;
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
// ...
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
   return [
       'email_verified_at' => 'datetime',
       'password' => 'hashed',
       'options' => AsEnumCollection::of(UserOption::class),
   ];
}

In Laravel 10, the same cast would look like this, since you cannot call static methods when defining an array property:

protected $casts = [
   'options' => AsEnumCollection::class.':'.UserOption::class,
];

This update is backward compatible with Laravel 10 and you can still define casts via the $casts property combined with the new casts() method. The $casts property and the casts() method are merged, and the method's keys take precedence over the $casts property. Although it is recommended to use the cast with the method version to take advantage of static methods.

A detailed guide to Eloquent, Laravel's ORM. Learn how to perform CRUD (Create, Read, Update, Delete) operations, configure your models with fillable and class attributes, and build complex queries.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español