Kennedy Mutisya
Putting Your Laravel Controllers on a Diet
Tame bloated controllers by extracting validation and save logic into dedicated form/action classes that keep HTTP layers thin.
A controller action that handles a registration form can balloon fast. You start with a simple save, add error handling, then validation, and before you know it you have twenty lines of glue code that the controller has no business owning.
Let us walk through how that happens and how to pull it back.
The Slow Descent
You start with the happy path:
public function store()
{
$user = new User(Input::all());
$user->save();
return View::make('account-created');
}
Straightforward, but fragile. So you add error handling:
public function store()
{
$user = new User(Input::all());
if (! $user->save()) {
return Redirect::to('/users/create')
->with('message', 'Something went wrong!');
}
return View::make('account-created');
}
Then someone submits without a password. So validation gets tacked on:
public function store()
{
$input = Input::all();
$rules = [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'confirmed', 'min:6'],
'first_name' => ['required'],
'last_name' => ['required'],
'date_of_birth'=> ['required', 'date'],
];
$validation = Validator::make($input, $rules);
if ($validation->fails()) {
return Redirect::to('/users/create')
->withErrors($validation)
->withInput();
}
$user = new User($input);
if (! $user->save()) {
return Redirect::to('/users/create')
->with('message', 'Something went wrong!');
}
return View::make('account-created');
}
Now the controller is stuffed with logic that has nothing to do with handling HTTP. It is defining form rules, running validations, managing redirects for multiple failure modes, and persisting records. None of that belongs here.
The Core Mistake
The trap is thinking of "model" as only "a class that maps to a database table." That thinking forces every piece of business logic into either a controller (wrong) or an Eloquent model (often wrong too, since your model should not care about form validation rules or redirect URLs).
You are allowed to make classes that do not correspond to tables. In fact, that is where most of your application's real code should live.
Extract a Form Class
Look at that controller again. The validation section is the biggest chunk of noise:
$input = Input::all();
$rules = array (...);
$validation = Validator::make($input, $rules);
if ($validation->fails()) {
return Redirect::to('/users/create')
->withErrors($validation)
->withInput();
}
All of that is about the form: what input it expects and whether that input is valid. So why is there not a UserRegistrationForm class?
class UserRegistrationForm
{
private $rules = [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'confirmed', 'min:6'],
'first_name' => ['required'],
'last_name' => ['required'],
'date_of_birth'=> ['required', 'date'],
];
private $attributes;
private $validation;
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}
public function isInvalid()
{
return ! $this->isValid();
}
public function isValid()
{
$this->validation = Validator::make(
$this->attributes,
$this->rules
);
return $this->validation->passes();
}
public function getValidation()
{
return $this->validation;
}
}
Now the controller looks like this:
public function store()
{
$form = new UserRegistrationForm(Input::all());
if ($form->isInvalid()) {
return Redirect::to('/users/create')
->withErrors($form->getValidation())
->withInput();
}
$user = new User(Input::all());
if (! $user->save()) {
return Redirect::to('/users/create')
->with('message', 'Something went wrong!');
}
return View::make('account-created');
}
Already a big improvement. The validation rules live with the form, not the controller. The controller just asks "is this valid?" and acts on the answer.
Going Further: Give the Form the Save Behaviour
Since the form already knows what valid input looks like, it might as well know how to turn that input into a user. Pull the save logic in too:
class UserRegistrationForm
{
// ...
public function save()
{
if ($this->isInvalid()) {
return false;
}
$user = new User($this->attributes);
return $user->save();
}
}
And now the controller becomes genuinely thin:
public function store()
{
$form = new UserRegistrationForm(Input::all());
if (! $form->save()) {
return Redirect::to('/users/create')
->withErrors($form->getValidation())
->withInput();
}
return View::make('account-created');
}
That is it. The controller handles HTTP input, decides what to do next, and returns a response. The form object handles validation and persistence. If the business rules change, you change the form object, not the controller.
What We Actually Did
This refactoring is just moving code to the right bucket. The form object is a plain PHP class with no framework inheritance. It does not extend Eloquent, it does not extend Controller. It represents the concept of "a user registration form" and knows what to do with the data it receives.
This is the foundation of patterns you will see everywhere in mature Laravel applications:
- Form requests: Laravel's own evolution of this idea, shipping validated input straight to your controller.
- Command objects: classes that encapsulate a single operation (register a user, place an order, cancel a subscription).
- Action classes: the same concept under a different name, popularised by Laravel 11's
make:action.
All of these patterns solve the same core problem: controllers should orchestrate, not implement. Push the logic downstream to plain PHP classes that are testable, reusable, and have one job.
Key Takeaways
- Do not equate "model" with "table." Your application is encouraged to have classes that do not map to database rows.
- Controllers should be thin. If your controller action has validation rules, error handling, user creation, and email sending, you have put everything in the wrong place.
- Form objects or command objects encapsulate a single operation and keep your controllers readable.
- When a controller action feels crowded, extract the domain logic into its own class first. The right name will make the code obvious.