As we approach 2025, Laravel continues to be a top choice for web development, offering developers the tools to create powerful and efficient applications. This article outlines key best practices, tips, and tricks that can enhance your code quality and streamline your workflow, whether you're using Laravel 9, 10, or 11. These insights will help you make the most of Laravel and improve your development skills, regardless of whether you are a beginner or an experienced developer.
Keep Laravel and Packages Updated
Keeping Laravel updated has several key benefits:
- Improved Security: Regular releases include important security fixes that protect your application.
- Better Performance: Updates often enhance performance, leading to faster load times and more efficient code.
- New Features and Functionality: Stay current to take advantage of new features that enhance the development experience.
- Compatibility: Ensure better compatibility with the latest official and community packages.
Regularly running composer update
is crucial for keeping your packages updated. This simple practice is one of the best ways to ensure your packages are up to date with the latest features and security patches.
If you are hesitant about updates, it may indicate that your code is not sufficiently tested. By adopting testing practices, you can ensure that major updates do not break your code, allowing you to fully leverage Laravel's improvements.
If you prefer not to update manually, consider using ServBay, an environment management tool that has already integrated PHP 8.4 beta versions, making it easier to keep Laravel and various packages updated.
Prevent Critical Errors by Maintaining Strong Testing
Automated testing is a broad yet often underestimated area. However, it is one of the most important best practices for ensuring project reliability.
Key benefits of maintaining a robust test suite include:
- Fewer Bugs: Regular testing helps catch issues early, leading to a more stable codebase.
- Happier Customers: Reliable software results in satisfied users who can trust your product.
- Satisfied Employers: Well-tested projects reflect professionalism and a commitment to quality.
- Confident Developers: With a comprehensive test suite, you can make changes without worrying about introducing new errors. Returning to a project after a break becomes less daunting.
When modifying code, simply run php artisan test
to identify any broken functionality, fix the issues, and repeat the process!
Automated testing can significantly enhance project stability, greatly boosting your career.
To help you get started, Laracasts offers free testing courses. You can learn about PHPUnit (the industry standard) and Pest (a modern testing framework that simplifies PHP testing). The "Pest From Scratch" course is highly recommended.
Follow Default Directory Organization
Have you ever considered why you should use a framework like Laravel?
Frameworks provide a structured approach to development, ensuring that all team members stay consistent and work towards a common goal. They come with rich, complex, battle-tested features, allowing you to focus on the unique aspects of your project without getting bogged down by details.
So, is following Laravel's default project structure considered a best practice? Absolutely! Here’s why:
- Convenience: Laravel's conventions and structure are well-documented. When you return to a project weeks or months later, you’ll appreciate the clarity and familiarity of the default setup.
- Team Collaboration: When working with team members, everyone understands Laravel's conventions, making collaboration easier. Leveraging this shared knowledge can propel the project forward, rather than reinventing the wheel at every turn.
Use Custom Form Requests for Complex Validation
Custom form requests in Laravel are powerful tools for handling validation, providing several key advantages:
- Reuse Validation Logic: Easily reuse validation rules across multiple controllers, keeping your code DRY (Don’t Repeat Yourself).
- Reduce Controller Bloat: Offload validation code to dedicated form request classes, keeping controllers clean and focused on business logic.
Creating a Custom Form Request
Creating a custom form request is simple. Just run the following Artisan command:
php artisan make:request StorePostRequest
Then, in your controller, you can simply type-hint the request:
use App\Http\Requests\StorePostRequest;
class PostController
{
public function store(StorePostRequest $request)
{
$validated = $request->validated();
Post::create($validated);
// Other logic...
}
}
Enhancing Custom Form Requests
Custom form requests can also include additional logic beyond validation. For instance, you can normalize input data before it reaches the controller by overriding the prepareForValidation
method:
public function prepareForValidation()
{
$this->merge([
'slug' => Str::slug($this->title),
]);
}
In this example, the method automatically generates a slug from the title before the validation process occurs.
Authorization Management
Custom form requests can also handle authorization logic. If you find using policies cumbersome, you can include simple authorization checks directly in the form request.
Simplify Your Codebase with Single Action Controllers
Even when following best practices, controllers can become unwieldy. A great solution is to use single action controllers in Laravel. These controllers are designed to handle a single action, rather than multiple actions like standard resource controllers.
Creating a Single Action Controller
To create a single action controller, use the following Artisan command:
php artisan make:controller ShowPostController --invokable
This command creates a controller with only one action, named __invoke
. For more information about the __invoke
magic method, refer to the Laravel documentation.
Defining Routes
With a single action controller, you can simplify route definitions. Here’s how to use it in your routes:
use App\Http\Controllers\ShowPostController;
// Single action controller route
Route::get('/posts/{post}', ShowPostController::class);
Subjective Best Practice
Using single action controllers is a subjective best practice. It’s up to you and your team to determine whether this approach fits the project's structure and readability preferences.
Use Middleware to Avoid Code Duplication
In Laravel, middleware is a powerful tool that allows you to filter or modify incoming requests. By leveraging middleware, you can avoid code duplication and keep controllers clean and manageable. Here are some common middleware use cases:
- Check Required Permissions: Ensure users have the necessary permissions to access certain routes.
- Set User Locale: Check the user's preferred language and adjust the application’s locale accordingly.
Laravel provides various built-in middleware for common tasks, such as authentication and rate limiting.
Creating Custom Middleware
To create custom middleware, use the following Artisan command:
php artisan make:middleware CheckTokens
In your middleware, you can define the logic to handle the request. Here’s an example of middleware that checks if a user has enough tokens:
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()->hasEnoughTokens()) {
abort(403); // Forbidden
}
return $next($request); // Continue to the next middleware or request handler
}
Attaching Middleware to Routes
Once you define your middleware, you can attach it to any number of routes. This modular approach helps keep your code clean and reduces duplication in controllers.
Route::middleware('check.tokens')->group(function () {
Route::get('/protected-resource', [ResourceController::class, 'index']);
Route::post('/protected-action', [ResourceController::class, 'store']);
});
Use Policies for Authorization
Utilizing policies for authorization in Laravel is crucial for building clear and maintainable applications. The three main benefits of using policies are:
- Shared Authorization Logic Across Controllers: By consolidating authorization rules into policies, you promote consistency and eliminate duplicate code in your application.
- Reduced Controller Complexity: Moving authorization logic to policies allows controllers to focus on their primary tasks, resulting in cleaner and more understandable code.
-
Simplified Access to Authorization Code: Policies are located in the
app/Policies
directory, making it easy for developers to find and modify authorization rules when needed.
Let’s look at a practical scenario of using a policy for post updates:
// app/Policies/PostPolicy.php
public function update(User $user, Post $post)
{
return $user->id === $post->user_id; // Verify if the user owns the post
}
// app/Http/Controllers/PostController.php
public function update(Request $request, Post $post)
{
$this->authorize('update', $post); // Check if the update action is authorized
// If authorized, proceed with the update process
// ...
}
Ensure Migrations are Up-to-Date
Migrations are a way to define the database schema using pure PHP code. You can think of them as a code alternative to phpMyAdmin, providing a programmatic way to manage your database structure. This approach is particularly beneficial for teams, as it allows everyone to replicate the same development environment on their local machines and maintain a clear history of changes in Git.
Migrations also facilitate deploying projects to new environments (like staging or production) without needing to export the database from other environments. However, a common pitfall is that some developers make changes directly to the database instead of creating new migrations. This practice can inconvenience other team members, who might need to request a database dump via Slack. To elevate the project and maintain consistency, it's important to effectively utilize migrations.
Use Anonymous Migrations to Prevent Conflicts (Laravel 8 and Above)
Anonymous migrations are an effective solution to avoid class name conflicts. With this feature, you can create multiple migrations named "update_posts_table" without encountering errors, reducing friction during the development process.
In Laravel 9 and above, when you run the following command, anonymous migrations are generated automatically:
php artisan make:migration UpdatePostsTable
The structure of these migrations looks like this:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration {
// Migration logic goes here
};
Interestingly, you can also implement anonymous migrations in Laravel 8. To do this, simply replace the class name with return new class
and ensure to include a semicolon at the end. This way, you can enjoy the benefits of anonymous migrations even in earlier versions.
Correctly Implement the down() Method for Rollbacks
The down()
method is used by the php artisan migrate:rollback
command to reverse changes made to the database. When some developers choose to implement it while others do not, it’s important to ensure that if you use it, your down()
method is defined correctly.
Essentially, the down()
method should reverse the operations performed in the up()
method. Here’s a basic example:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration {
public function up()
{
Schema::table('posts', function (Blueprint $table) {
// Change column from boolean to datetime.
$table->datetime('is_published')->nullable()->change();
});
}
public function down()
{
Schema::table('posts', function (Blueprint $table) {
// Restore the column to its original state during rollback.
$table->boolean('is_published')->default(false)->change();
});
}
};
If you prefer not to use the down()
method, you can simply delete it.
Follow Eloquent's Database Table Naming Conventions
Adhering to Laravel's table naming conventions is a simple best practice that can greatly benefit your team. When you use Artisan commands, the framework automatically handles these conventions, such as php artisan make:model Post --migration --factory
.
If, for some reason, you cannot use these commands, here’s a quick guide:
-
For a model named Post, the corresponding table should be named posts, using the plural form. For example:
- Comment model → comments table
- Reply model → replies table
-
For a pivot table connecting Post and Comment (e.g., comment_post):
- Use the singular form of both names.
- Arrange them in alphabetical order.
For more details, refer to the official documentation.
Avoid the N+1 Problem by Using Eager Loading
There are more best practices to cover! Have you encountered the N+1 problem? Eager loading is an effective way to prevent them.
For instance, if you display a list of 30 posts and their authors, due to lazy loading, Eloquent will execute one query for the 30 posts and then an additional 30 queries for each author (which means every time you call $post->user
in your code, it retrieves the user relationship).
The solution is simple: use the with()
method to reduce the number of queries from 31 to 2.
Post::with('author')->get();
To prevent N+1 problems, you can configure the application to throw an exception whenever a relationship is lazily loaded. This restriction should only be enforced in local environments.
Model::preventLazyLoading(
// Return `true` unless in production environment.
! app()->isProduction()
);
Use Eloquent's Strict Mode to Avoid Performance Issues and Errors
Eloquent's strict mode is a valuable debugging tool. It helps developers identify potential issues during development by throwing exceptions in various scenarios:
- Lazy Loaded Relationships: Lazy loading can lead to performance issues, especially when dealing with large datasets. This situation occurs when related models are fetched from the database only when accessed. In strict mode, an exception will be thrown if a relationship is lazy loaded, encouraging the use of eager loading.
-
Assigning Unfillable Attributes: The
$fillable
property in Eloquent models prevents mass assignment vulnerabilities. If you attempt to assign unfillable attributes, an exception will be triggered, ensuring developers remain cautious when handling mass assignment. - Accessing Non-existent Attributes: Attempting to access attributes that do not exist or have not been retrieved from the database can lead to unpredictable behavior or errors. Strict mode will throw exceptions in such cases, helping developers locate and resolve these issues.
To enable strict mode, add the following code to the boot()
method of AppServiceProvider.php
:
Model::shouldBeStrict(
// It will only enable outside of the production environment.
! app()->isProduction()
);
Use the New Method to Define Accessors and Mutators
Laravel 9 introduced a new way to declare accessors and mutators. Here’s how to implement them now:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Pokemon
{
public function name(): Attribute
{
$locale = app()->getLocale();
return Attribute::make(
get: fn($value) => $value[$locale],
set: fn($value) => [$locale => $value],
);
}
}
This new syntax allows you to define accessors and mutators in a more concise way. You can also cache computationally expensive values:
use Illuminate\Database\Eloquent\Casts\Attribute;
public function someAttribute(): Attribute
{
return Attribute::make(
fn() => /* perform some operation */
)->shouldCache();
}
In contrast, the previous method looked like this:
class Pokemon
{
public function getNameAttribute(): string
{
$locale = app()->getLocale();
return $this->attributes['name'][$locale];
}
public function setNameAttribute($value): string
{
$locale = app()->getLocale();
return $this->attributes['name'][$locale] = $value;
}
}
Switching to the new syntax simplifies the code and enhances readability.
Use dispatchAfterResponse() to Handle Long-Running Tasks
When dealing with time-consuming tasks (like sending emails after submitting a contact form), you can improve user experience by delaying the task until after the server responds to the user. This is where dispatchAfterResponse()
comes into play, allowing you to postpone long-running processes:
SendContactEmail::dispatchAfterResponse($input);
Alternatively, if you prefer to schedule tasks using an anonymous function, you can do so like this:
dispatch(function () {
// Execute some long-running tasks here.
})->afterResponse();
By using dispatchAfterResponse()
, you ensure that users receive a faster response from the server while background tasks do not affect the user experience.
Use Queues to Handle Long-Running Tasks
When processing compute-intensive tasks (like handling user-uploaded images), executing these operations immediately can overload the server and lead to a poor user experience. Here’s how to address it:
- Server Pressure: Submitting real-time processing tasks for every user can overwhelm the server.
- User Experience: Users are forced to wait, staring at a loading screen, leading to frustration.
Laravel's queue system allows you to defer these long-running tasks to run in an ordered or controlled parallel manner, ensuring the server remains responsive and users do not have to wait.
To facilitate task management, consider using Laravel Horizon, which provides a user interface for efficiently monitoring and controlling queue tasks. This way, you can handle complex processes in the background while maintaining smooth performance.
Lazily Refresh the Database Before Each Test
When testing in a local environment, it's best to use a fresh database each time you run tests. Laravel provides an efficient way to handle this by lazily refreshing the database before each test. This can be accomplished using the RefreshDatabase
trait in your test class. This ensures that the database is in a clean state each time tests are run, eliminating potential issues caused by leftover data.
Using RefreshDatabase
Use the RefreshDatabase
trait in your test class:
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
public function test_example()
{
// Test logic...
}
}
Advantages
- Consistency: Ensures that the database state is consistent each time tests are run, avoiding test failures due to state changes.
- Isolation: Each test runs independently, without interference, enhancing the reliability of the tests.
Securely Manage Sensitive Information
When managing sensitive information, ensuring security is crucial. Here are some best practices:
Use Environment Variables
Store sensitive information (like API keys and database credentials) in the .env
file rather than in the codebase.
Use Laravel's Encryption Features
For data that needs encryption, utilize Laravel's encryption features:
use Illuminate\Support\Facades\Crypt;
// Encrypt
$encrypted = Crypt::encryptString('sensitive information');
// Decrypt
$decrypted = Crypt::decryptString($encrypted);
Regularly Update Keys
Ensure to regularly update application keys (APP_KEY
) and re-encrypt data after updates.
Conclusion
By following these best practices, tips, and tricks, you can significantly improve the quality and security of your Laravel applications. These methods will not only help you write more efficient code, but also provide a better experience for your users. Continuing to learn and apply these practices will help you achieve greater success in your development.
Top comments (0)