DEV Community

Cover image for 8 Laravel Quick Tips You Can Use Today
devTalk
devTalk

Posted on Originally published at dev-talk.com

8 Laravel Quick Tips You Can Use Today

Some Laravel features are famous, and some sit in the docs for years while people write the same longer code around them. These eight are small, each takes under a minute to try, and each one removes either a chunk of code or a whole class of bug. I ran every example on Laravel 13.35.0 with a real database, so the outputs below are what actually came back.

1. See the real SQL with toRawSql()

You want to know what query a chain of conditions produces. Instead of reading the query log, ask the builder:

Post::where('status', 'draft')
    ->where('reads', '>', 5)
    ->orderBy('id')
    ->limit(10)
    ->toRawSql();

// select * from `posts` where `status` = 'draft' and `reads` > 5 order by `id` asc limit 10
Enter fullscreen mode Exit fullscreen mode

The values are already filled in, so you can paste the result straight into a database client and run EXPLAIN on it. The query builder also has dumpRawSql() and ddRawSql() for printing it in the middle of a chain (I confirmed these exist in the framework source; I ran toRawSql() itself). Treat the output as debugging information only and keep it out of logs on production if the values may be private.

2. Use sole() when exactly one row must exist

first() happily returns the first match, even when you expected only one. sole() insists:

Post::where('title', 'A')->sole();   // returns the model
Post::where('title', 'B')->sole();   // two rows exist
Post::where('title', 'Z')->sole();   // none exist
Enter fullscreen mode Exit fullscreen mode

My results: one match returned the model, two matches threw MultipleRecordsFoundException ("2 records were found."), and zero matches threw the usual ModelNotFoundException. Good for lookups like "the one active subscription for this user" where a duplicate means your data is broken and you want to know now.

3. isDirty() before saving, wasChanged() after

These two get mixed up all the time. The test shows the difference:

$post->title = 'New title';

$post->isDirty('title');      // true  (changed, not saved yet)
$post->wasChanged('title');   // false

$post->save();

$post->isDirty('title');      // false (nothing pending now)
$post->wasChanged('title');   // true  (it changed during the last save)
Enter fullscreen mode Exit fullscreen mode

Use isDirty() in code that runs before the save, and wasChanged() afterwards, for example in an observer's updated method when you only want to send an email if the status changed.

4. Copy a record with replicate()

Need a "duplicate this post" button? replicate() gives you an unsaved copy:

$copy = $post->replicate();   // exists = false, id = null, created_at = null
$copy->title = 'Copy of ' . $post->title;
$copy->save();                // inserted as a new row

$copy = $post->replicate(['status']);   // skip the status attribute
Enter fullscreen mode Exit fullscreen mode

The primary key and timestamps are not copied, and the title came across, as I verified. The array argument lists attributes to leave out. Relationships are not copied, so duplicate child rows yourself if you need them.

5. Update extra columns while incrementing

A page-view counter often also needs a "last viewed" time. One query does both:

$post->increment('reads', 1, ['last_read_at' => now()]);
Enter fullscreen mode Exit fullscreen mode

The row afterwards had reads = 1 and the new last_read_at. Because it is one atomic UPDATE, two requests arriving together do not overwrite each other the way $post->reads++; $post->save(); can. One thing to remember from the timestamps article: increment() also bumps updated_at unless you wrap it in Model::withoutTimestamps().

6. Build optional filters with when()

Instead of a stack of if statements around your query:

$posts = Post::query()
    ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
    ->when($filters['min_reads'] ?? null, fn ($q, $min) => $q->where('reads', '>=', $min))
    ->get();
Enter fullscreen mode Exit fullscreen mode

With a null status and a minimum of 3, the SQL was just select * from posts where reads >= 3. The empty filter added nothing. One catch: when() skips a value that is falsy, so a filter value of 0 or '0' is ignored. If zero is a valid choice, check explicitly with ->when($min !== null, ...).

7. Run something once per object with once()

If a method does expensive work and gets called several times, wrap the body in once():

public function heavy()
{
    return once(function () {
        // expensive work
        return $this->calculate();
    });
}
Enter fullscreen mode Exit fullscreen mode

I called such a method three times on one object and the closure ran once. A second object ran its own closure once, so the cached result belongs to each object and does not leak between them. That also means it lasts only as long as the object does.

8. Turn on strict mode in development

This is the one I would add first to any project. Model::shouldBeStrict() switches on three safety checks together:

// app/Providers/AppServiceProvider.php, in boot()
Model::shouldBeStrict(! $this->app->isProduction());
Enter fullscreen mode Exit fullscreen mode

What each check did in my test:

  • Lazy loading: looping over Post::get() and touching $post->comments threw LazyLoadingViolationException ("Attempted to lazy load [comments] on model [Post] but lazy loading is disabled."). That is the classic N+1 problem, caught during development. Loading a single model with first() and then its relation did not throw, so the check targets models loaded together.
  • Silently dropped attributes: new Post(['title' => 'x', 'reads' => 5]) where reads is not fillable threw MassAssignmentException instead of quietly ignoring it.
  • Missing attributes: selecting only id and then reading $post->title threw MissingAttributeException instead of returning null.

Running it only outside production (as above) is a common choice, because you want these mistakes to fail loudly while you build, not break a live page. After I turned strict mode off, Post::with('comments') loaded the same data without any errors, which is the usual fix for a lazy loading violation.


Originally published on DEV Talk.

Top comments (0)