Laravel Controllers 202: Advanced Techniques and Best Practices

Samuel AgyeiSamuel Agyei
7 min read

Building on the fundamentals of Laravel controllers, this guide takes a deep dive into advanced techniques that enhance the way controllers manage application logic, handle routing, interact with models, and improve code maintainability. While the basics like CRUD operations and middleware are crucial, Laravel offers powerful features for streamlining your controllers even further.


Table of Contents

  1. Recap: Laravel Controller Basics
  2. Form Requests and Validation
  3. Dependency Injection in Controllers
  4. Controller Namespaces and Grouping
  5. API Controllers and Responses
  6. Advanced Route Model Binding
  7. Controller Traits and Code Reusability
  8. Invokable Controllers for Single Responsibility
  9. Using View Composers with Controllers
  10. Controller Testing and Debugging
  11. Conclusion

1. Recap: Laravel Controller Basics

Before diving into more advanced topics, let’s briefly revisit the basics:

  • Controllers act as intermediaries between routes and models/views, handling requests and delegating logic.
  • Controllers should keep logic clean and minimal, using models or services for heavy lifting.
  • Route Model Binding allows you to inject model instances into your controller methods directly.
  • Middleware can be applied at the controller or route level to filter incoming requests.

If you need a quick refresher, the Laravel Controllers 101 guide offers a solid foundation.


2. Form Requests and Validation

When building applications with forms, you’ll frequently need to validate user input. Instead of cluttering your controller with validation logic, Laravel’s Form Requests allow you to extract validation rules into separate classes, improving code organization.

Creating a Form Request

You can create a form request using Artisan:

php artisan make:request UpdateProfileRequest

This will generate a new file under the app/Http/Requests directory. Here’s how you might define validation rules:

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateProfileRequest extends FormRequest
{
    public function authorize()
    {
        // Authorization logic here, usually return true
        return true;
    }

    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email,' . $this->user()->id,
            'password' => 'nullable|min:8|confirmed',
        ];
    }
}

Using the Form Request in a Controller

In your controller method, instead of manually writing validation logic, you can inject the form request:

use App\Http\Requests\UpdateProfileRequest;

class UserController extends Controller
{
    public function update(UpdateProfileRequest $request)
    {
        $user = auth()->user();
        $user->update($request->validated());

        return redirect()->back()->with('status', 'Profile updated!');
    }
}

Benefits:

  • Keeps the controller clean.
  • Centralizes validation logic for easier testing and reuse.
  • Allows for custom authorization logic.

3. Dependency Injection in Controllers

Dependency Injection (DI) allows you to inject dependencies directly into your controller’s constructor or methods. This can include services, repositories, or even other classes.

Injecting Services

Suppose you have a service that handles sending notifications, called NotificationService. You can inject it into your controller like this:

use App\Services\NotificationService;

class NotificationController extends Controller
{
    protected $notificationService;

    public function __construct(NotificationService $notificationService)
    {
        $this->notificationService = $notificationService;
    }

    public function sendNotification()
    {
        $this->notificationService->send(auth()->user(), 'Hello!');
        return response()->json(['message' => 'Notification sent']);
    }
}

With DI, Laravel automatically resolves the dependencies from the service container. This approach leads to more modular and testable code.


4. Controller Namespaces and Grouping

As your application grows, organizing your controllers becomes increasingly important. Laravel allows you to group controllers by namespace and folder for better structure.

Organizing Controllers by Domain

You can create subdirectories within the Controllers folder to group related controllers. For example, if you have an admin section, you might have a structure like this:

app/Http/Controllers/Admin
app/Http/Controllers/User
app/Http/Controllers/Auth

Defining Namespaces in Routes

When defining routes, you can group them by namespace to avoid specifying the full namespace in every route:

Route::namespace('Admin')->group(function () {
    Route::get('dashboard', [DashboardController::class, 'index']);
    Route::resource('users', 'UserController');
});

This keeps your route definitions clean and organized.


5. API Controllers and Responses

Laravel offers several tools for building APIs, and API-specific controllers often differ from those used in traditional web apps. Instead of returning views, API controllers typically return JSON responses.

Creating an API Controller

To generate an API controller, use the --api flag:

php artisan make:controller Api/ProductController --api

This will generate a controller with only the necessary CRUD methods (index, store, show, update, destroy).

Returning JSON Responses

API controllers usually return JSON responses rather than views. Here’s an example of a simple API response:

use App\Models\Product;

class ProductController extends Controller
{
    public function show(Product $product)
    {
        return response()->json($product);
    }

    public function store(Request $request)
    {
        $product = Product::create($request->all());

        return response()->json([
            'message' => 'Product created successfully!',
            'data' => $product
        ], 201);
    }
}

API Resource Controllers

You can also use API Resources to format and standardize your responses. Laravel’s API Resources offer a clean way to structure API responses, which can be particularly useful for handling complex data relationships.


6. Advanced Route Model Binding

While implicit route model binding (which automatically injects models based on route parameters) is powerful, sometimes you need more control over how models are resolved.

Customizing Implicit Binding

By default, Laravel binds route parameters to model IDs. However, you can customize this behavior by overriding the getRouteKeyName() method on your model:

class Product extends Model
{
    public function getRouteKeyName()
    {
        return 'slug'; // Bind by 'slug' instead of 'id'
    }
}

Now, when you define a route like this:

Route::get('/products/{product}', [ProductController::class, 'show']);

Laravel will automatically resolve the Product by its slug field.

Explicit Binding

If you need even more control, you can define explicit route model bindings in the RouteServiceProvider:

public function boot()
{
    parent::boot();

    Route::bind('product', function ($value) {
        return Product::where('slug', $value)->firstOrFail();
    });
}

7. Controller Traits and Code Reusability

As you build more complex applications, you’ll notice that some code in your controllers gets duplicated across multiple controllers. One way to reduce redundancy is by using traits.

Example: Shared Functionality with Traits

Let’s say you have multiple controllers that need to handle file uploads. You can extract this logic into a trait:

trait FileUploadTrait
{
    public function uploadFile($file)
    {
        $filename = $file->getClientOriginalName();
        return $file->storeAs('uploads', $filename);
    }
}

Now, you can use this trait in any controller that needs file upload functionality:

use App\Traits\FileUploadTrait;

class UserController extends Controller
{
    use FileUploadTrait;

    public function store(Request $request)
    {
        $path = $this->uploadFile($request->file('avatar'));
        return response()->json(['path' => $path]);
    }
}

8. Invokable Controllers for Single Responsibility

When a controller only handles a single task, it’s a good idea to use invokable controllers. This keeps your code clean and focused on a single responsibility.

Creating an Invokable Controller

To generate an invokable controller, run:

php artisan make:controller SendNotificationController --invokable

This creates a controller with a single __invoke method:

class SendNotificationController extends Controller
{
    public function __invoke()
    {
        // Handle the notification logic here
    }
}

You can define routes for invokable controllers like this:

Route::post('/send-notification', SendNotificationController::class);

9. Using View Composers with Controllers

When working with controllers that return views, you may want to share data across multiple views without repeating the same logic. View composers are a clean way to accomplish this.

Defining

a View Composer

In a service provider (usually AppServiceProvider), you can bind data to specific views:

use Illuminate\Support\Facades\View;

public function boot()
{
    View::composer('layouts.app', function ($view) {
        $view->with('notifications', auth()->user()->notifications);
    });
}

This will automatically pass the notifications variable to every view that uses the layouts.app template.


10. Controller Testing and Debugging

Testing your controllers ensures that your application behaves as expected. Laravel’s built-in testing tools make it easy to write tests for your controllers.

Writing Controller Tests

In Laravel, you can create controller tests using the php artisan make:test command. For example:

php artisan make:test UserControllerTest

Here’s a basic test for the index method of a controller:

class UserControllerTest extends TestCase
{
    public function testIndexReturnsUsers()
    {
        $response = $this->get('/users');

        $response->assertStatus(200);
        $response->assertViewHas('users');
    }
}

Laravel’s testing tools provide a range of methods for asserting HTTP responses, views, and data passed to them.


11. Conclusion

Advanced techniques in Laravel controllers can significantly improve your application’s scalability, readability, and maintainability. By leveraging features like form requests, dependency injection, traits, and view composers, you can keep your controllers lean and focused on their core responsibilities.

By organizing your controllers into logical namespaces, using API-specific responses, and embracing route model binding, you’ll build a solid architecture that is easy to maintain and extend. These strategies, combined with thorough testing, will ensure that your Laravel application grows efficiently and remains adaptable to future changes.

1
Subscribe to my newsletter

Read articles from Samuel Agyei directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samuel Agyei
Samuel Agyei

I am a full-stack developer. I am passionate about programming in general. I love to read documentaries and code