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! 🤘