Clean Laravel Controllers: Best Practices and Tips, part 1

Moumen Alisawe
7 min readMar 12, 2023

--

Laravel is a powerful PHP framework that provides developers with a robust set of tools for building web applications. However, as your application grows in complexity, it can become difficult to maintain clean and organized code.

In this article, we’ll explore some best practices and tips for writing clean and maintainable Laravel controllers.

Keep Controllers Simple and Focused:

One of the most important principles of writing clean Laravel controllers is to keep them simple and focused. A controller should only be responsible for one thing, such as handling a specific HTTP request or performing a specific action.

For example, let’s say you have a UserController that’s responsible for handling requests related to user accounts. Instead of cramming all user-related actions into a single controller, consider breaking them down into separate controllers for actions like viewing profiles, updating user information, and resetting passwords.

Here’s an example of a UserController that’s responsible for handling profile-related requests:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
public function show(Request $request, User $user)
{
return view('users.show', compact('user'));
}

public function edit(Request $request, User $user)
{
return view('users.edit', compact('user'));
}

public function update(Request $request, User $user)
{
$user->update($request->all());

return redirect()->route('users.show', $user);
}
}

In this example, the UserController is only responsible for handling requests related to viewing, editing, and updating user profiles. By keeping the controller focused, the code becomes more readable and easier to maintain.

2. Move Business Logic to Action or Service Classes:

Laravel Controllers tend to become bloated as the application grows, making them hard to maintain and test. As a solution, it’s recommended to extract the business logic into Action or Service classes.

Actions

An Action class is a simple class that performs a single task or action within the application. It encapsulates a single piece of business logic, such as validating form data or sending an email. Actions are typically used in response to a specific HTTP request, and they allow you to keep your Controllers clean and focused on handling the request and returning a response.

Here’s an example of an Action class that validates a user’s registration data:

namespace App\Actions\Auth;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
class RegisterUser
{
public function execute(array $data)
{
$validator = Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|confirmed|min:8',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
auth()->login($user);
return redirect()->route('dashboard');
}
}

In this example, the RegisterUser class encapsulates the logic for validating and registering a new user. The execute method takes an array of form data as a parameter, validates it using Laravel's built-in Validator class, and creates a new User model if the data is valid. The method then logs the user in and redirects them to the dashboard.

To use this Action in a controller, you can instantiate it and call the execute method:

namespace App\Http\Controllers\Auth;

use App\Actions\Auth\RegisterUser;
use Illuminate\Http\Request;

class RegisterController extends Controller
{
public function store(Request $request, RegisterUser $registerUser)
{
$registerUser->execute($request->all());

return redirect()->route('dashboard');
}
}

By using an Action class, the RegisterController is simplified, and the business logic for validating and registering a new user is encapsulated in a dedicated class.

Services

A Service class is a more general-purpose class that encapsulates a set of related business logic for interacting with external systems, such as databases, APIs, or third-party services. Services are typically used across multiple controllers, models, or other classes, and they provide a single point of entry for interacting with external systems.

Here’s an example of a Service class that encapsulates the logic for interacting with a payment gateway API:

namespace App\Services;

use Stripe\Charge;
use Stripe\Customer;

class PaymentService
{
public function createCustomer($email, $token)
{
$customer = Customer::create([
'email' => $email,
'source' => $token,
]);

return $customer;
}

public function chargeCustomer($amount, $customer, $description)
{
$charge = Charge::create([
'amount' => $amount,
'currency' => 'usd',
'customer' => $customer,
'description' => $description,
]);

return $charge;
}
}

In this example, the PaymentService class encapsulates the logic for creating and charging a customer using the Stripe API.

The createCustomer method creates a new customer object in Stripe and returns it, while the chargeCustomer method charges the specified amount to the customer's saved payment method and returns the resulting charge object.

To use this Service in a controller, you can instantiate it and call its methods:

namespace App\Http\Controllers;

use App\Services\PaymentService;
use Illuminate\Http\Request;

class PaymentController extends Controller
{
public function charge(Request $request, PaymentService $paymentService)
{
$customer = $paymentService->createCustomer(
$request->input('email'),
$request->input('stripeToken')
);

$charge = $paymentService->chargeCustomer(
1000,
$customer->id,
'Test Charge'
);

return view('payments.success', compact('charge'));
}
}

By using a Service class, the PaymentController is simplified, and the business logic for interacting with the Stripe API is encapsulated in a dedicated class. This makes it easy to reuse the same logic in other parts of the application or swap out the Stripe API with a different payment gateway if needed.

Overall, moving business logic to dedicated Action or Service classes can help keep your Laravel Controllers clean and focused on handling HTTP requests and returning responses. It also makes it easier to test and maintain your code and promotes code reuse across different parts of your application.

3. Use Route Model Binding

Laravel’s Route Model Binding feature is a powerful tool that can simplify your controller code. Instead of manually fetching a model instance from the database, you can use Route Model Binding to automatically fetch the model instance based on the URL parameter.

For example, let’s say you have a UserController that’s responsible for handling requests related to user accounts. Instead of manually fetching the user model instance from the database, you can use Route Model Binding to automatically fetch it based on the URL parameter:

namespace App\Http\Controllers;

use App\Models\User;

class UserController{
public function show(User $user)
{
return view('users.show', compact('user'));
}

public function edit(User $user)
{
return view('users.edit', compact('user'));
}

public function update(UpdateUserProfileRequest $request, User $user, UpdateUserProfileAction $action)
{
$action->execute($user, $request->validated());
return redirect()->route('users.show', $user);
}

In this example, the UserController methods use Route Model Binding to automatically fetch the user model instance based on the `user` parameter in the URL. This simplifies the code and makes it easier to read.

4. Use Form Requests:

Form requests are another helpful feature provided by Laravel. They allow you to encapsulate request data and logic into reusable objects. Using form requests helps you to keep your controllers clean and focused. You can move the validation logic from your controller to the form request. Here’s an example:

class CreatePostFormRequest extends FormRequest
{
public function rules()
{
return [
'title' => 'required|string',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
];
}

public function authorize()
{
return true;
}
}

class PostController extends Controller
{
public function store(CreatePostFormRequest $request)
{
$post = new Post();
$post->title = $request->input('title');
$post->content = $request->input('content');
$post->category_id = $request->input('category_id');
$post->save();

return redirect()->route('posts.index');
}
}

In the example above, we’ve created a CreatePostFormRequest class that extends the FormRequest class provided by Laravel. In this class, we've defined the validation rules for the incoming request. We've also overridden the authorize method to return true to allow any authenticated user to create a post.

In the PostController's store method, we've type-hinted the CreatePostFormRequest to validate the incoming request. We've then used the validated method to get the validated input data, which we've used to create a new Post instance and save it to the database.

By using form requests, we’ve moved the validation logic out of the controller and into a dedicated class, which makes the code more modular and easier to test. Additionally, the CreatePostFormRequest can be reused in other parts of the application that require similar validation logic.

5. Use Dependency Injection:

Dependency injection is a technique for decoupling components in your application. It allows you to write code that is more modular, testable, and easier to maintain. In Laravel, you can use dependency injection in your controllers to make them more flexible and easier to test.

Here’s an example:

class UserController extends Controller
{
private $userService;

public function __construct(UserService $userService)
{
$this->userService = $userService;
}

public function index()
{
$users = $this->userService->getAll();

return view('users.index', compact('users'));
}

public function show(User $user)
{
$userData = $this->userService->getUserData($user);

return view('users.show', compact('userData'));
}
}

In this example, we’ve injected a UserService instance into our UserController via the constructor. We've then used this service in our controller's index and show methods to retrieve and display user data. By using dependency injection, we've decoupled our controller from the service, making it more flexible and easier to test. We can easily swap out the UserService for a different service if needed, without changing our controller's code.

In addition to services, you can also use dependency injection to inject other dependencies, such as repositories, validators, or event dispatchers. By using dependency injection, you can write code that is more modular, testable, and easier to maintain.

Conclusion:

In this article, we’ve discussed some best practices and tips for building clean Laravel controllers. By following these practices, you can make your controllers more manageable, modular, and easier to test.

We’ve covered the importance of keeping controllers small and focused, using route model binding, avoiding business logic in controllers, using form requests or custom request objects, and using dependency injection.

By following these best practices, you can write controllers that are easier to understand, test, and maintain. You can also make your controllers more modular, making it easier to reuse code across your application.

In part 2 of this series, we’ll cover more best practices and tips for building clean Laravel controllers. We’ll discuss topics such as authorization, exception handling, and more. Stay tuned for the next part!

--

--

Moumen Alisawe

Software engineer with 6+ years developing mobile and web apps. Passionate about creating seamless user experiences. Let's build something great together.