How to Review Laravel Code
Good code is like a good conversation ... clear, concise, and easy to follow.
When it comes to reviewing Laravel code, this principle couldn't be more true. Code reviews aren't just about finding errors; they're about ensuring your project remains secure, scalable, and maintainable over time.
While preferences may vary among teams, there are proven best practices that lead to consistently better outcomes when it comes to code review. In this article, we'll dive into the key elements of an effective Laravel code review.
So let's dive in. 👇
1. Ensure Code Follows Best Practices
Laravel is a framework with a well-defined set of best practices, and code reviews should ensure these are followed. Key practices to check for:
- Coding standards: Ensure that the code adheres to PSR-2 and PSR-4 standards for coding style and structure. Having a common code style makes it easier to onboard new developers.
Tools to use:
- Larastan (PHPStan for Laravel): Adds code analysis to Laravel improving developer productivity and code quality.
- Laravel Pint: Laravel’s code style fixer to help automatically fix issues.
- Naming Conventions: Confirm that classes, methods, and variables use clear, descriptive names. For example, Eloquent models should follow singular naming (User, Order), while controller methods should describe actions (store, update, etc.).
- Method Length: Avoid overly long methods. Break them into smaller, reusable chunks if they are doing too much.
- Separation of Concerns: Make sure that the code adheres to the single responsibility principle (SRP). Controllers shouldn’t handle business logic directly, this should be managed by services classes.
Example:
// Bad: Too many responsibilities and unclear logic public function createOrder(Request $request) { $user = User::find($request->user_id); $order = new Order(); $order->user_id = $user->id; $order->total = $request->total; $order->save(); Mail::to($user->email)->send(new OrderConfirmation($order)); }In the above example, logic to send an email should be extracted to a service or event, improving clarity and responsibility segregation.
// Improved version public function createOrder(CreateOrderRequest $request, OrderService $orderService) { $order = $orderService->create($request->validated()); return response()->json(['order' => $order]); } - Consistent Indentation and Formatting: This includes consistent use of spacing, alignment, and indentation. Clean formatting improves readability significantly.
- Avoid Magic Strings and Numbers: Instead of using magic strings or numbers throughout the codebase, ensure constants or configuration files are used.
if ($user->role == 'admin') { // 'admin' could be defined in a constant $discount = $total * 0.15; }Instead:
class RoleConstants { const ROLE_ADMIN = 'admin'; } if ($user->role === self::ROLE_ADMIN) { // ... } - Clear, Concise Comments: There’s a fine line between useful and redundant comments. Ensure comments are added where logic isn’t immediately apparent but avoid unnecessary or self-explanatory comments.
- Example:
// This loop iterates through each user foreach ($users as $user) { $this->sendNotification($user); }Instead:
// Notify users about the new policy changes foreach ($users as $user) { $this->sendNotification($user); }
2. Use Design Patterns
-
Service Pattern:
When business logic doesn’t belong directly in controllers or models, extract it into service classes. This keeps controllers focused on handling requests while encapsulating reusable logic in dedicated services.
What to look for during review:
-
Bloated controllers or models with too many responsibilities.
-
Repeated logic that could be moved into a service.
// Controller interacting with the service layer $order = $this->orderService->processOrder($request->all()); -
- Factory Pattern:
Factories create objects in a clean and reusable way, particularly when object creation involves complex logic or dependencies. Laravel’s model factories are great for seeding and testing, but you can also define custom factories for other use cases.
What to look for during review:
-
Repeated or conditional object creation code.
-
Opportunities to encapsulate creation logic in factories.
$reportGenerator = ReportFactory::create($reportType); -
- Strategy Pattern
The strategy pattern allows you to define multiple interchangeable algorithms for a specific task and switch between them dynamically. It’s especially useful for handling different payment methods, notification channels, or pricing rules.
What to look for during review:
-
Long
if/elseorswitchstatements deciding behavior. -
Repeated logic for similar but slightly different workflows.
$paymentStrategy = $this->strategyFactory->resolve($paymentMethod); $paymentStrategy->pay($order); -
- Observer Pattern
The observer pattern helps you react to model events (like
created,updated, ordeleted) in a clean, decoupled way. Laravel’s built-in model observers and events/listeners make this easy to implement.What to look for during review:
-
Logic tied to model lifecycle events placed inside controllers or models.
-
Repeated triggers that could be centralized.
Order::observe(OrderObserver::class);
-
- Decorator Pattern
This pattern allows you to dynamically add behavior to an object without modifying its core code, which is ideal for tasks like caching, logging, or enhancing API responses.
What to look for during review:
-
Repeated logic added around the same methods.
-
Features like caching or metrics added inconsistently.
$orderService = new CachingOrderService(new OrderService()); -
3. Events & Listeners
Laravel has robust support for event-driven architecture with events and listeners. Instead of cramming too much into controllers or services, developers should be using events to decouple various parts of the application. As part of your review:
-
Are events being fired for long-running or background tasks? For example, sending emails, calculating reports, or triggering notifications should be handled asynchronously using events.
// Bad: Direct email sending in a controller Mail::to($user->email)->send(new OrderConfirmation($order)); // Good: Dispatch job to queue SendOrderConfirmation::dispatch($order); -
Are listeners lightweight and focused? Ensure that listeners don’t contain complex logic. Push this to service classes if necessary.
// Fire an event event(new OrderPlaced($order));// Listener that handles background work class SendOrderConfirmation implements ShouldQueue { public function handle(OrderPlaced $event) { Mail::to($event->order->user->email)->send(new OrderConfirmation($event->order)); } }
Worth noting:
Using too many events can turn your Laravel application into a circus. While events decouple logic and improve maintainability, overuse can lead to a chaotic, hard-to-follow flows. Developers should be mindful of this balance to avoid an overcomplicated architecture.
3. Use Dependency Injection (DI) Everywhere
Proper use of Dependency Injection is crucial for testability and flexibility. During code review, ensure that:
-
Controller dependencies are injected through the constructor, not hardcoded inside methods.
// Good: Dependency injection in the constructor public function __construct(UserService $userService) { $this->userService = $userService; }
- Services and Helpers are injected where necessary, instead of using app() or global facades in the code. This improves testability and clarity about class dependencies.
// Avoid this app(UserService::class)->getAll(); // Use DI instead public function __construct(UserService $userService) { $this->userService = $userService; }
5. Use Rate Limiting and Throttling
For APIs or forms, review the usage of rate limiting and throttling to prevent abuse and denial-of-service attacks. Laravel makes it easy to implement these with the throttle middleware.
-
API Rate Limiting: Ensure API routes are rate-limited to prevent abuse.
Route::middleware('throttle:60,1')->group(function () {
Route::get('/user', 'UserController@show');
});
-
Login/Signup Throttling: Check if user login or signup endpoints use throttling to avoid brute-force attacks.
6. Security Best Practices
Security is critical in any Laravel application. During a code review, pay special attention to potential vulnerabilities:
- SQL Injection: Ensure all database queries are parameterized or use Eloquent to prevent raw SQL queries that might allow SQL injection.
- Input Validation: Ensure all incoming data is validated before being processed or stored. Use Laravel’s request validation rules to sanitize and validate inputs, reducing the risk of unexpected data causing issues.
- File Upload Security: Check that uploaded files are validated for allowed file types, sizes, and content. Store files outside of the public directory to prevent unauthorized access, and rename files to avoid executable code being stored with malicious extensions.
- Password Storage and Policies: Confirm that passwords are hashed using secure algorithms (such as bcrypt or Argon2) rather than plain text. Also, consider enforcing strong password policies for better security.
- Logging and Monitoring: Ensure the application logs essential security events (like failed logins or permission denials) without logging sensitive information. Set up monitoring tools to alert on suspicious activities.
- Cross-Site Scripting (XSS): Review the code for proper escaping of user inputs, especially when outputting data into views. Use {{ }} for escaping variables in your blade views and avoid {{!! !!}} unless absolutely necessary.
- CSRF Protection: Ensure that forms use Laravel’s built-in CSRF token protection.
- Sensitive Data Exposure: Make sure no sensitive information (like passwords, API keys, or database credentials) is hard-coded or accidentally exposed.
Example:
// Bad: Vulnerable to SQL Injection
DB::select("SELECT * FROM users WHERE email = '$email'");
Instead, use Eloquent or parameterized queries:
// Good: Secure
User::where('email', $email)->first();
7. Performance Considerations
During your code review, keep an eye on the efficiency of the code. Poor performance can significantly affect user experience and scalability.
- Database Queries: Ensure that there are no unoptimized queries. Look for instances of the "N+1" problem, where a loop triggers multiple queries. Utilize Laravel’s eager loading capabilities with with() to load related models in one query.
- Caching: Encourage the use of caching for frequently accessed data to reduce database load.
- Indexes: Review database migrations and ensure that columns frequently used in queries are properly indexed.
- Pagination for Large Result Sets: Ensure that large datasets are paginated rather than loaded all at once. Laravel provides paginate() to handle this efficiently, minimizing resource usage and improving load times.
Example:
// Bad: N+1 problem in a loop
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
// Good: Using eager loading to fix N+1
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
8. Test Coverage and Code Reliability
Testing is a crucial aspect of any code review. While aiming for 100% test coverage isn’t always practical, and can even slow down productivity, it's essential to ensure that all core functionality is reliably covered by automated tests. The goal isn’t to cover every single line of code but to guarantee that key features and workflows are protected against unexpected issues.
When deploying a new version of your application, you want confidence that the new changes won’t disrupt your app. A solid test suite helps you stay on the safe side by catching potential issues early and preventing downtime in production.
Here’s what you should focus on:
-
Unit Tests: Ensure there is comprehensive coverage of the core logic and critical functions with unit tests. These tests help guarantee that individual components perform as expected.
-
Feature and Integration Tests: Verify that all major features and integrations are thoroughly tested. These tests are essential for catching issues across different parts of the application and ensuring seamless functionality.
Example:public function test_create_order() { $response = $this->post('/orders', [ 'user_id' => 1, 'total' => 100 ]); $response->assertStatus(201); $this->assertDatabaseHas('orders', [ 'user_id' => 1, 'total' => 100, ]); }
- Mocking and Dependency Injection in Tests: Mock dependencies during tests to isolate units of work, ensuring that tests don’t depend on external services.
// Example of mocking a service in a test $mock = Mockery::mock(UserService::class); $mock->shouldReceive('kissUser')->once()->andReturn(collect([$user1])); $this->app->instance(UserService::class, $mock);
- Database Transactions in Tests: Use database transactions to keep your database clean while running integration tests. Laravel automatically rolls back database changes made during testing.
<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\User; class ExampleTest extends TestCase { // Use the RefreshDatabase trait to handle transactions use RefreshDatabase; public function test_example() { // Arrange: Set up any necessary data $user = User::factory()->create([ 'name' => 'John Doe', ]); // Act: Perform the action being tested $response = $this->actingAs($user)->get('/profile'); // Assert: Verify the expected outcome $response->assertStatus(200); $this->assertDatabaseHas('users', [ 'name' => 'John Doe', ]); } }
9. Refactor Opportunities
Identify areas in the code that may need refactoring:
- Repetitive Code: Consolidate duplicated code into reusable components, traits, or helper methods.
- Fat Controllers: Move logic out of controllers into service classes, jobs, or events to keep controllers slim.
- Large Models (Fat Models): Move complex logic from models to service classes or repositories. Models should ideally focus on data structure and relationships, not heavy business logic.
- Unnecessary Dependencies: Remove any unused libraries or dependencies in composer.json to avoid bloat.
- Nested Loops: Simplify or refactor nested loops into separate methods or consider using collections to improve readability and performance.
- Tightly Coupled Code: Reduce dependencies between classes by using dependency injection or design patterns like the Factory or Repository pattern. This makes code easier to test and maintain.
- Monolithic Views: Break down large views into smaller, reusable components. This reduces duplication and improves the maintainability of frontend code.
10. Enforce SOLID Principles
During a code review, one of the most valuable checks you can perform is verifying that the code adheres to the SOLID principles. These five principles are the foundation of clean, maintainable, and scalable object-oriented design.
They ensure that your Laravel codebase remains flexible and easier to extend as your application grows.
-
Single Responsibility Principle (SRP)
Each class, method, or component should have only one reason to change — meaning it should handle a single, well-defined responsibility. Violating this rule often leads to bloated classes that try to “do everything,” making them hard to maintain and test.
What to look for during review:
-
Large controllers or models mixing multiple types of logic (e.g., validation, business rules, data access).
-
Service classes that perform unrelated tasks.
-
Methods that are hundreds of lines long or handle multiple processes.
Recommendation:
Encourage breaking such code into smaller, focused classes or methods. For example, move business logic into service classes, data logic into repositories, and background tasks into jobs. -
-
Open/Closed Principle
Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to introduce new functionality without changing existing code — which reduces the risk of breaking things unintentionally.
What to look for during review:
-
Repeated edits to core classes when adding new features.
-
Condition-heavy logic (
if,switch) that could be replaced by polymorphism or strategies.
Recommendation:
Use interfaces, abstract classes, or strategy patterns to extend behavior instead of rewriting existing logic. For instance, adding a new payment method should mean creating a new strategy class — not modifying existing payment logic. -
-
Liskov Substitution Principle (LSP)
Subclasses should be completely substitutable for their parent classes without altering the expected behavior. This ensures that inheritance is logical and that polymorphism works as intended.
What to look for during review:
-
Subclasses overriding methods in ways that break the parent’s contract.
-
Inconsistent return types or unexpected side effects in overridden methods.
Recommendation:
Ensure subclasses truly represent a specialized version of their parent. If not, consider composition over inheritance, which includes injecting dependencies instead of extending classes. -
-
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don’t use. In other words, keep interfaces small and focused.
What to look for during review:
-
Large interfaces or abstract classes defining unrelated methods.
-
Implementations that leave many methods empty or unused.
Recommendation:
Split monolithic interfaces into smaller, more specific ones. For example, instead of a singleUserServiceInterfacewith dozens of methods (login, register, delete, notify, etc.), create separate interfaces likeAuthenticatesUsers,RegistersUsers, andDeletesUsers. -
Conclusion
Code reviews in Laravel aren’t just about catching bugs or following rules, they’re about making the project better for everyone who works on it.
Focus on keeping the code clean, easy to read, and secure. Every improvement you make now saves headaches later and helps your team move faster in the future.
So, dive in with an eye for detail, and keep building apps that you, and your team, can be proud of!
Happy coding!