May 13th, 2023 Laravel PHP

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 Controllers 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 Controllers 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 Controllers 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 Controllers 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 Controllers 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!