Laravel Best Practices
In this article, we’ll explore these essential Laravel best practices, from structuring your code to optimizing database operations, ensuring your projects stay efficient and developer-friendly.
Whether you’re a seasoned Laravel developer or just starting, these practices will help you level up your development skills and deliver high-quality applications.
So let's dive in. 👇
Fat Models, Skinny Controllers
Shift database logic to Eloquent models to maintain cleaner controllers and reusable code
Bad example:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($query) {
$query->where('created_at', '>', now()->subDays(7));
}])
->get();
return view('index', compact('clients'));
}
Good example:
public function index(Client $client)
{
return view('index', ['clients' => $client->getVerifiedWithRecentOrders()]);
}
class Client extends Model
{
public function getVerifiedWithRecentOrders(): Collection
{
return $this->verified()
->with(['orders' => fn($query) => $query->recent()])
->get();
}
public function scopeVerified($query)
{
return $query->where('is_verified', true);
}
}
class Order extends Model
{
public function scopeRecent($query)
{
return $query->where('created_at', '>', now()->subDays(7));
}
}
Single Responsibility Principle
A class should have only one responsibility. This means that a class should focus on a single piece of functionality. Violating this principle can make your code harder to read, test, and maintain because it mixes concerns that should be separated.
By adhering to the Single Responsibility Principle, you create code that is easier to understand and refactor. Each class or service has a clear purpose, making the overall system more modular and flexible.
Bad example:
public function update(Request $request): string
{
$validated = $request->validate([
'name' => 'required|max:255',
'tasks' => 'required|array:due_date,status'
]);
foreach ($request->tasks as $task) {
$formattedDate = $this->carbon->parse($task['due_date'])->toDateTimeString();
$this->logger->info('Task updated: ' . $formattedDate . ' - ' . $task['status']);
}
$this->project->updateTasks($request->validated());
return redirect()->route('projects.index');
}
Good example:
public function update(UpdateProjectRequest $request): string
{
$this->taskLogger->logTasks($request->tasks);
$this->projectService->updateTasks($request->validated());
return redirect()->route('projects.index');
}
class TaskLogger
{
public function logTasks(array $tasks): void
{
// Logic to log tasks
}
}
class ProjectService
{
public function updateTasks(array $data): void
{
// Logic to update project tasks
}
}
Methods Should Do Just One Thing
A function should have a single purpose and execute it well. When a method does more than one thing, it becomes harder to understand, test, and maintain. Splitting responsibilities into smaller, focused methods makes your code more readable and easier to debug.
Bad example:
public function getFullNameAttribute(): string
{
if (auth()->user() && auth()->user()->hasRole('admin') && auth()->user()->isVerified()) {
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}
Good Example:
public function getFullNameAttribute(): string
{
return $this->isVerifiedAdmin() ? $this->formatFullName() : $this->formatShortName();
}
private function isVerifiedAdmin(): bool
{
$user = auth()->user();
return $user && $user->hasRole('admin') && $user->isVerified();
}
private function formatFullName(): string
{
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
}
private function formatShortName(): string
{
return strtoupper($this->first_name[0]) . '. ' . ucfirst($this->last_name);
}
Keep Business Logic in Service Classes
Controllers should only handle HTTP requests and responses, delegating complex logic to service classes. This keeps code clean, reusable, and easier to test.
Bad example:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$image = $request->file('image');
$image->storeAs('temp', $image->getClientOriginalName(), 'public');
}
// Other unrelated logic...
}
Good example:
public function store(Request $request, ArticleService $articleService)
{
$articleService->uploadImage($request->file('image'));
// Other unrelated logic...
}
class ArticleService
{
public function uploadImage(?UploadedFile $image): void
{
if ($image) {
$image->storeAs('uploads/temp', uniqid() . '_' . $image->getClientOriginalName(), 'public');
}
}
}
Avoid Business Logic in Routes
Routes should only handle HTTP requests, not business logic. This keeps your code clean and maintainable.
Bad example
// Business logic in the route
Route::post('/article', function (Request $request) {
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->save();
});
Good example
// Route delegates logic to the controller
Route::post('/article', [ArticleController::class, 'store']);
// In ArticleController
public function store(Request $request)
{
// logic to create article
}
Use Relationships for Cleaner Code
Use Eloquent relationships to simplify and clarify how related models interact. This avoids repetitive assignments and makes the code easier to maintain and less error-prone.
Bad example
$article = new Article;
$article->title = $request->input('title');
$article->content = $request->input('content');
$article->verified = $request->boolean('verified');
$article->category_id = $category->id;
$article->save();
Good example
$category->articles()->create($request->safe()->only(['title', 'content', 'verified']));
Use Database Transactions for Atomic Business Operations
Transactions ensure all database operations succeed or fail as a group, maintaining data integrity.
Bad example
public function placeOrder(Request $request)
{
$order = new Order;
$order->user_id = $request->user_id;
$order->save();
$payment = new Payment;
$payment->order_id = $order->id;
$payment->save();
}
Good example
use DB;
public function placeOrder(Request $request)
{
DB::beginTransaction();
try {
$order = Order::create($request->validated());
$payment = Payment::create(['order_id' => $order->id]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
Avoid Queries in Blade: Use Eager Loading
Executing queries inside Blade templates leads to inefficient database calls, especially with loops. Eager loading fetches related data in a single query, improving performance and avoiding the N + 1 query problem.
Bad example
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
If you have 100 users, this triggers 101 queries: one for the users and one for each user's profile.
Good example
// in a service class or model passed back to controller which shares that with the blade file
$users = User::with('profile')->get();
// in blade file
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
This triggers only 2 queries: one for users and one for their profiles.
Chunk Data for Performance
For tasks that involve large datasets, processing in chunks reduces memory usage and improves performance by limiting the amount of data held in memory at once.
Bad example
$users = User::all();
foreach ($users as $user) {
// Process each user
}
Good example
User::chunk(500, function ($users) {
foreach ($users as $user) {
// Process each user
}
});
Use Constants Instead of Hardcoded Values
Using constants will help you locate the places where this value is used in case you wanted to change it, refactor and will help you with debugging.
Bad example
public function isAdmin(User $user): bool
{
return $user->type === 'admin';
}
Good example
public function isAdmin(User $user)
{
return $user->type === UserType::ADMIN;
}
Translate Strings
You'll thank yourself in the future as your app grows if you considered translating strings from the start. All you need it to pass strings through the __()
function
Bad example
return back()->with('message', 'Your article has been added!');
Good example
return back()->with('message', __('Your article has been added!')); // notice the call to __()
Inject Dependencies
Creating instances with new
tightly couples your classes and makes them harder to test or modify. Using the IoC container allows for easier dependency injection and better testability.
Bad example
public function store(Request $request)
{
$user = new User;
$user->create($request->validated());
}
Good example
public function __construct(protected UserService $userService) {}
public function store(Request $request)
{
$this->userService->create($request->validated());
}
Avoid Directly Using .env
in Code
Accessing data directly from the .env
file throughout your application can make your code harder to maintain and test. Instead, store values in configuration files and retrieve them using config()
.
Bad example
$apiKey = env('API_KEY');
Good example
// config/services.php
'api_key' => env('API_KEY'),
// Retrieve the value
$apiKey = config('services.api_key');
Store Dates as Objects, Not Strings
Storing dates as strings can lead to inconsistent formats and parsing errors. It's better to store them as Carbon instances, which provide robust date handling. Use accessors and mutators to format dates only when needed in the display layer.
Bad example
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Good example
// In Model
protected $casts = [
'ordered_at' => 'datetime',
];
// In Blade View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->format('m-d') }}
Keep Code Documentation Minimal and Meaningful
Excessive documentation often clutters the code and makes it harder to maintain. Instead, rely on clear, descriptive names for variables, functions, and classes. Use comments only when absolutely necessary to explain complex logic.
Bad example
/**
* The function checks if the given string has any white spaces
*
* @param string $string String received from frontend which might contain
* space characters. Returns True if the string
* is valid.
*
* @return bool
*
* @license GPL
*/
public function checkString($string)
{
}
Good example
public function hasWhiteSpaces(string $string): bool
{
}
Align on Coding Standards with Your Team
Consistent code improves readability and maintainability, making collaboration easier.
Use can Laravel Pint to automatically format and enforce coding standards. It integrates with your development workflow, so you can run it before each commit using Git Hooks.
composer require --dev laravel/pint
vendor/bin/pint
Test, Test and Test
And finally, one of the most important things you can do to ensure the reliability, maintainability, and scalability of your code is to write automated tests.
You don't need 100% coverage of your functionality (I'd argue that's counter productive), but at least you need to make sure that all you GET routes are covered, and the more you can add on top, the better.
Here’s why:
-
Catch Bugs Early: Tests help identify issues before they make it to production. Catching bugs during development is far cheaper and easier than fixing them post-deployment.
-
Code Confidence: With proper test coverage, you can make changes to the codebase with confidence. Tests ensure that new changes don't break existing functionality.
-
Documentation: Well-written tests act as living documentation for your code. They describe how the system is expected to behave and can be used to understand the code's intent.
-
Refactoring Made Safe: When refactoring or improving existing code, tests provide a safety net that ensures no functionality is lost during changes.
-
Improved Design: Writing tests often encourages better software design. To write testable code, you typically end up with smaller, more focused methods and classes that are easier to maintain.
-
Collaboration: Tests make it easier for teams to work together on large codebases. They define clear expectations for behavior, reducing misunderstandings and improving collaboration.
-
Continuous Integration: Tests are essential for implementing continuous integration and delivery workflows. Automated tests can run on every code push, ensuring that only stable code is deployed.
-
Long-Term Maintenance: In large projects, tests help maintain stability over time, especially when the team changes. New developers can rely on tests to understand the behavior of the codebase and ensure future changes don't break functionality.
Keep pushing, keep building! 🤘