Hidden Laravel Gems: Laravel Tips Every Developer Should Know

By Amas
Hidden Laravel Gems: Laravel Tips Every Developer Should Know

Laravel is packed with powerful features and elegant APIs, but some of its best tricks are hiding in plain sight.  Hidden in plain sight. These lesser-known tricks can make your development workflow smoother, faster, and more enjoyable.

Whether you're scaling a serious SaaS or experimenting with something new, there's no excuse for wasting time on problems Laravel has already solved. These tips cut through the noise and show you how to make the most of the framework: cleaner code, fewer hacks, and more time spent actually building.

So, let's dive in 👇

Use withRelationshipAutoloading() to Avoid N+1 Problems

The N+1 problem in Laravel occurs when a query retrieves a set of results, and then additional queries are made for each result, leading to inefficient database calls. This typically happens when eager loading isn’t used, causing Laravel to run individual queries for each relationship in a collection.

To avoid this, you'd use eager loading with methods like with() to load related data in a single query, improving performance and reducing unnecessary database overhead.

// With eager loading
$orders = Order::with('client.owner.company')->get();

foreach ($orders as $order) {
    echo $order->client->owner->company->name; // Only two queries: one for orders and one for the relationships
}

In Laravel 12.8, a new method, withRelationshipAutoloading(), has been added to models and Eloquent collections. When called, it automatically loads relations whenever they are accessed, without the need for explicit load() or with() calls.

$orders = Order::all()->withRelationshipAutoloading();

foreach ($orders as $order) {
    echo $order->client->owner->company->name;
}


Also, you can activate eager loading for all models using the following call (In an application provider)

public function boot() {
    Model::AutomaticallyEagerLoadRelationships();
}

Pretty handy, isn't it?

Use lazy() Instead of get() for Large Datasets

When processing a lot of rows, lazy() can dramatically reduce memory usage:

User::lazy()->each(function ($user) {
    // Do something with each user
});

It loads the records one by one using a cursor under the hood.

 

Use defer() to Cleanup After a Response

Starting from version 10, Laravel lets you delay logic until after the response is sent:

defer(function () {
    // Clean up temporary files
    Storage::delete($this->tempFile);
});

Perfect for non-urgent background work that doesn’t need a queue, like deleting a file, sending emails or storing request analytics. 

 

Use Route Model Binding in Laravel

Route Model Binding is a powerful feature in Laravel that allows you to automatically inject models into your route callbacks. Instead of manually fetching the model using an ID or another identifier, Laravel will automatically query the model and inject it into your route action. 

By default, Laravel resolves the model by its primary key (usually id). However, you can customize this behavior to resolve models using different fields, such as a unique slug, username, or email.

Route::get('/users/{user}', fn(User $user) => $user);

You can customize the key used for route model binding. For example, if you have a route that accepts a blog post's slug:

Route::get('blog/{slug}', [BlogController::class, 'show']);

Normally, you'd fetch the post manually in the controller:

use App\Models\Post;

class BlogController extends Controller
{
    public function show($slug)
    {
        $post = Post::where('slug', $slug)->firstOrFail();

         // .... do something with the post
    }
}

To simplify this, you can update the route to bind by the slug directly:

Route::get('blog/{post:slug}', [BlogController::class, 'show']);

Now, Laravel automatically resolves the Post model using the slug and injects it into the controller method:

class BlogController extends Controller
{
    public function show(Post $post)
    {
         // ... do something with post
    }
}


Custom Casts for Clean Models

Custom casts allow you to encapsulate logic directly within your model, keeping it clean and reusable. With a custom cast, you can define how an attribute is stored and retrieved, improving both functionality and clarity.

Example:

class Uppercase implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes)
    {
        return strtoupper($value);
    }

    public function set($model, string $key, $value, array $attributes)
    {
        return strtolower($value);
    }
}

Usage:

protected $casts = [
    'username' => Uppercase::class,
];

 

Artisan Tinker Aliases

Tinker supports global aliases! Create a .tinker.php file in your project root:

use App\Models\User;

function admin() {
    return User::where('email', '[email protected]')->first();
}

Now you can call admin() directly in Tinker.

Debug Slow Queries with DB::listen()

Easily monitor and log all SQL queries in your application to identify performance bottlenecks.

Example:

DB::listen(function ($query) {
    logger($query->sql, $query->bindings, $query->time);
});

This is especially useful in services or controllers to track slow or inefficient queries, helping you debug and optimize your database interactions.


Reuse Validation Logic with Rule::when()

In Laravel, you can simplify conditional validation logic using Rule::when(). This allows you to apply rules dynamically based on certain conditions, making your validation logic cleaner and more readable. Unlike sometimes(), which can become messy with complex conditions, Rule::when() offers a more straightforward approach to conditionally applying validation rules.

Use conditional logic inside rule definitions:

use Illuminate\Validation\Rule;

Rule::when($user->isAdmin(), ['required', 'email']);

 

Use password Rule for Secure Password Checks

Laravel has a built-in password rule that can enforce strong password policies:

'password' => ['required', 'string', Password::min(8)->letters()->symbols()],

No need to manually write regex anymore.

 

Use artisan model:prune to Auto-Clean Old Records

Want to delete old logs or temporary records?

In your model:

use Illuminate\Database\Eloquent\Prunable;

public function prunable()
{
    return $this->where('created_at', '<', now()->subMonth());
}

Then add this to your schedule:

$schedule->command('model:prune')->daily();

This will remove the records that are older than 1 month. 

 

Define defaults() in Routes

Pre-fill route parameters so your URLs stay clean:

Route::get('reports/{year}/{month}', ReportController::class)
    ->defaults('year', now()->year)
    ->defaults('month', now()->month);

Now /reports will automatically fill with the current year and month if omitted.

 

Use Model::withoutEvents() When You Need Silence

Sometimes you want to update or create a model without triggering observers/events:

User::withoutEvents(function () {
    User::factory()->count(10)->create();
});

Super useful for seeding or silent admin actions.

 

Eloquent’s fresh() and refresh() Are Not the Same

  • refresh() reloads the model in-place from the database.

  • fresh() returns a new instance.

$user->name = 'John';
$user->refresh(); // Will discard changes and reload

$newUser = $user->fresh(); // $user still has changes, $newUser doesn't

Subtle difference, big impact.

 

Use password.confirm Middleware for Sensitive Routes

Want to protect profile or billing pages?

Route::middleware(['auth', 'password.confirm'])->group(function () {
    Route::get('/billing', BillingController::class);
});

Laravel will ask users to re-enter their password before continuing. 🔐

 

Queue a Closure with dispatch(function () {})

Not every background task requires a full-fledged Job class in Laravel. For quick, one-off tasks, you can dispatch a closure directly to the queue. This keeps things lightweight and simple without the overhead of creating a separate job class:

dispatch(function () use ($user) {
    Mail::to($user)->send(new WelcomeMail($user));
});

Super handy for small, one-off background tasks.

 

Use Route::fallback() to Catch-All Unmatched Routes

Handle 404s gracefully or redirect stray requests:

Route::fallback(function () {
    return response()->view('errors.404', [], 404);
});

Or log them, redirect to a friendly landing, or fire off a notification.

 

Use tap() for Fluent Side Effects

Use tap() when you want to perform an action during a chain, without breaking it:

$user = tap(User::find(1), function ($user) {
    Log::info("Fetched user: {$user->email}");
});

Keeps things readable and functional.

 

Use ->diffForHumans() on Dates

Laravel makes your dates human-readable effortlessly:

$user->created_at->diffForHumans(); // e.g., "3 hours ago"


Use retry() Helper for Resilient Operations

Ideal for handling flaky APIs, database locks, or unreliable services, the retry() helper makes your code more robust by automatically retrying failed operations.

Example:

return retry(3, function () {
    return Http::get('https://external-api.com/data');
}, 100); // retries 3 times, 100ms delay

This adds built-in resiliency to your app, ensuring smoother interactions with unreliable external services. 💪


Conclusion

While Laravel offers a lot out of the box, understanding and leveraging these advanced features can set you apart as a developer who doesn't just use the framework but knows how to make it work to its full potential. Don’t hesitate to dig deeper into these tools, they’re meant to help you build better, faster, and more maintainable applications.

Keep shipping great software! 🤘

Share this post.
Liked that? Subscribe to our newsletter for more
Ship fast & don't reinvent the wheel

Build your SaaS using SaaSykit

SaaSykit is a SaaS boilerplate that comes packed with all components required to run a modern SaaS software.

Don't miss this

You might also like