DEV Community

westtan
westtan

Posted on

Laravel Best Practices, Tips, and Tricks for 2025

Image description

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
Enter fullscreen mode Exit fullscreen mode

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...
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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']);
});
Enter fullscreen mode Exit fullscreen mode

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
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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();
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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()
);
Enter fullscreen mode Exit fullscreen mode

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()
);
Enter fullscreen mode Exit fullscreen mode

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],
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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...
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)