Controllers and their true purpose
Last week, I tweeted about how my controllers look in my own applications and how I generally approach them. The tweet quickly went viral and gained a lot of attention, but unfortunately for all the wrong reasons. That's why in this blog post, I'd like to shed some light on what I was actually aiming for and explain what a (UI) Controller
is supposed to be in general.
The code examples are from the source code of this blog. Please head over to the repository to see how the different pieces click together.
Refactoring the (UI) Controller
The discussions below the tweet were mainly about Dependency Injection (DI) vs. Service Location (SL). However, this was not the point of the tweet in any way, shape or form. So let's take a minute to refactor the code to make use of SL:
final readonly class SubmitContactFormController
{
public function __invoke(SubmitRequest $request): RedirectResponse
{
$command = new ContactMuhammed(
$request->input('email'),
$request->ip(),
$request->input('message'),
$request->input('name'),
);
Bus::dispatch($command);
return Redirect::route('contact', ['success' => true]);
}
}
The steps we took are:
- The form validation rules have been moved to a
FormRequest
class - The
Bus
component is now used through a static container proxy - The
ResponseFactory
component is now used through a static container proxy
Now that we have successfully refactored the (UI) Controller
to make use of SL, we can start talking about its intended purpose.
The (UI) Controller as the Composition Root
The (UI) Controller
actually has a distinguished role during our application's lifecycle. The web server usually receives a request, forwards it to a PHP process which boots the framework and finally the framework forwards the request to us: the (UI) Controller
. Based on this, we can establish the fact that a (UI) Controller
is the Composition Root of our application. It is the first piece of code that is called which we have full control over.
This is the reason why I don't really mind whether you make use of DI or SL in your (UI) Controller
. The object graph has to be composed somehow, so feel free to use either of them.
Why do you keep prefixing "UI" to Controller?
I'm glad you asked. That's because I'd like to put emphasis on this very fact: a UI Controller
s task is to orchestrate the Request
-Response
lifecycle which is usually initiated by a user through a user interface. The keyword here is orchestration. Its primary goal should be to handle UI related concerns such as form validation, rendering a view, creating a redirect etc. If the Controller
holds onto its true purpose, then it doesn't really matter whether it's 10 lines long, or 90 lines long. It should handle the UI concerns, and handle them well. Everything else does not belong inside a Controller
and should be forwarded to the Application
.
This might sound a bit counter-intuitive now, but a CLI Command
is a Controller
as well. It also takes user input and does something with it, albeit in a slightly different manner. Livewire components? Yep, they're Controller
s as well. Just dynamic ones leveraging XHR on the front-end.
Forwarding messages to the application
When a request comes into our Controller
, something must happen. Someone tried to invoke some particular behavior in our system. The intent of the user is represented by the command object, or in other words the application is represented by this one single command object:
$command = new ContactMuhammed(
$request->input('email'),
$request->ip(),
$request->input('message'),
$request->input('name'),
);
The Controller
s relationship with the Application
starts and ends here. It forwards the message, in this case the command, to the application and calls it a day. ContactMuhammed
is the contract between the Controller
and the Application
handling this command. As long as the contract is respected and stays intact, everything will keep functioning without a hitch. This is what one calls "loose coupling" and it was the entire point of my original tweet.
Now, I am deliberately keeping the Application
as abstract as possible because the implementation details can vary from person to person and from codebase to codebase. Some people like implementing Clean Architecture (I call that the "Baklava" Architecture, mmm), some people like to vertically slice their applications and some people like to mix and match.
Isn't that the "Action" pattern?
No, it is not. The Action
pattern is a rechristened version of the GoF Command pattern which represents a self-handling command.
If we take a closer look at ContactMuhammed
, we can see that there's 0 business logic embedded inside of it:
final readonly class ContactMuhammed implements ShouldQueue
{
public function __construct(
public string $email,
public string $ipAddress,
public string $message,
public string $name,
) {}
}
ContactMuhammed
is what we could call an EIP Command. It represents the intent of the user, and only that. Nothing more. Eagle-eyed readers may already have noticed that is also in fact a Data Transfer Object
, albeit a more specific one.
What about the query side of things?
It's true that we have only talked about commands thus far. However, querying some data and returning that to the user doesn't change anything regarding the Controller
s design. While commands could be handled asynchronously by the Application
, queries are typically synchronous and thus we are temporally coupled to our Application
.
This is the logic that is responsible for rendering this very blog post:
final readonly class ReadBlogPostController
{
public function __construct(
private GetSinglePost $posts,
private ResponseFactory $response,
) {}
public function __invoke(string $slug): Response
{
$post = $this->posts->findBySlug($slug);
return $this->response->view('read-blog-post', $post->toArray());
}
}
GetSinglePost
is the contract between the Controller
and our Application
. As long as it keeps returning a Post
view model, everything will stay functional and nothing will break.
Summary
- The
Controller
s main task is to handle the User Interface - The
Controller
should delegate everything else to the application - The
Controller
should return user friendly error messages in case of failures
Join the discussion on X (formerly Twitter)! I'd love to know what you thought about this blog post.
Thanks for reading!