Laravel Magazine
5 Eloquent Tricks to Write Cleaner, Faster Laravel Code

5 Eloquent Tricks to Write Cleaner, Faster Laravel Code

Eric Van Johnson ·

Eloquent is one of those things in Laravel where you can get surprisingly far knowing only the basics, and then one day you discover a method that makes you question every model you have ever written. These five features do not get nearly enough attention, but once you start using them you will wonder how you managed without them.

1. sole() — When You Expect Exactly One Result

You probably use first() all the time, but first() is silent when there are multiple matching records. It just returns whichever row the database feels like giving you. That is rarely what you actually want.

sole(), added in Laravel 9, throws an exception if the query returns zero records or more than one. It enforces the contract that you expect exactly one result.

// Throws ModelNotFoundException if not found
// Throws MultipleRecordsFoundException if more than one exists
$invoice = Invoice::where('reference', $ref)->sole();

This is perfect for things like unique token lookups, activation links, or any place where "more than one result" means something has gone wrong and you want to know about it rather than silently return the wrong record.

2. withCount() and withAggregate() — Stop Loading Relationships Just to Count Them

A classic N+1 variant: you load a collection of posts, then call $post->comments->count() inside a loop. This fires a query per post to load all the comments just to count them.

withCount() adds a {relation}_count attribute to each model with a single extra query:

$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count; // No extra query
}

For more complex aggregates, withAggregate() lets you pull any column value from a related table:

$users = User::withAggregate('latestOrder', 'created_at')->get();
// Adds latest_order_created_at to each user

Or use the convenience wrappers withSum(), withMax(), withMin(), and withAvg():

$products = Product::withSum('orders', 'quantity')->get();
// Adds orders_sum_quantity to each product

3. value() and pluck() — Stop Calling ->get() When You Only Need One Column

When you only need a single column value from a single row, calling ->first()->column_name is wasteful. It selects all columns and instantiates a full model object. Use value() instead:

// Instead of this:
$name = User::where('id', $id)->first()->name;

// Do this:
$name = User::where('id', $id)->value('name');

value() returns the raw scalar value — no model instantiation, minimal data transferred.

When you need a single column from multiple rows, pluck() gives you a clean collection without building full model objects:

$emails = User::where('active', true)->pluck('email');
// Returns Collection(['alice@example.com', 'bob@example.com', ...])

You can pass a second argument to key the collection by another column:

$names = User::pluck('name', 'id');
// Returns Collection([1 => 'Alice', 2 => 'Bob', ...])

4. lazy() and lazyById() — Process Large Datasets Without Blowing Up Memory

Processing thousands of rows with get() loads them all into memory at once. chunk() is better, but it runs a separate COUNT query and can miss records if you modify the dataset mid-chunk. lazy() and lazyById() are the modern answer.

lazy() uses a PHP generator to yield records one at a time using cursor-based pagination:

User::where('active', true)->lazy()->each(function (User $user) {
    // Process one at a time, constant memory usage
    $user->sendWeeklyDigest();
});

lazyById() is even safer for large datasets. It chunks by ID rather than using OFFSET, which avoids the classic problem of offset-based pagination slowing down as you get deeper into large tables:

User::lazyById(200)->each(function (User $user) {
    // Processes in batches of 200, ordered by primary key
});

Both work seamlessly with pipeline-style collection methods:

User::where('trial', true)
    ->lazyById()
    ->filter(fn ($u) => $u->trialExpiredDaysAgo() > 3)
    ->each->sendConversionEmail();

5. whenLoaded() in API Resources — Safe Conditional Relationship Output

If you have written API resources, you have probably run into the situation where sometimes a relationship is eagerly loaded and sometimes it is not, and you need your resource to handle both cases without triggering lazy loads.

whenLoaded() is the right tool for this. It only includes the relationship data in the response if it was already eager-loaded, and omits it entirely otherwise:

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'       => $this->id,
            'title'    => $this->title,
            'author'   => new UserResource($this->whenLoaded('author')),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'tags'     => TagResource::collection($this->whenLoaded('tags')),
        ];
    }
}

This means the resource is safe to use whether you loaded the relationship or not. No surprise lazy queries, no undefined property errors, and the response shape adapts to what was actually loaded.

Pair it with a conditional eager load based on a request parameter and you have a cheap, flexible sparse fieldsets implementation without a full JSON:API library:

$post = Post::when(
    request('include') === 'comments',
    fn ($q) => $q->with('comments')
)->findOrFail($id);

return new PostResource($post);

These five features are all in Laravel right now, no extra packages needed. Pick one, drop it into your next PR, and watch your teammates wonder how you got so much cleaner code out of the same Eloquent they use every day.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.