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: For business logic that doesn't fit neatly into models or controllers, consider using service classes. During the review, identify bloated controllers or models that could benefit from the service pattern.
// Controller interacting with the service layer $order = $this->orderService->processOrder($request->all());
-
Factory Pattern: This is useful for creating objects in a more flexible and scalable way, especially when complex object creation logic is required.
3. Events & Lisenteners
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
Review the code to ensure adherence to SOLID principles. These principles ensure clean, maintainable, and scalable code:
-
Single Responsibility Principle (SRP): Each class should have one responsibility. If you notice bloated classes or methods, encourage breaking them down into smaller, more focused components.
-
Open/Closed Principle: Code should be open for extension but closed for modification. Ensure that new functionality can be added without altering existing code.
-
Interface Segregation: Large interfaces should be split into smaller, more specific ones. For example, avoid monolithic service interfaces that combine multiple unrelated actions.
Conclusion
Wrapping up a Laravel code review is more than just checking off items on a list. It’s a chance to make a real impact on the quality and future-proofing of your application.
By focusing on clean architecture, maintainable & readable code, and security, you’re not only improving the current project but also building a stronger foundation for future work among your team.
So, dive in with an eye for detail, and keep building apps that you, and your team, can be proud of!
Happy coding!