Symfony2 controllers and application logic, revisited

Got this email today from “a loyal French reader,” (thanks for the compliment!) and I thought I’d go over the answer in today’s post:

> … I just read your articles about Symfony2 controllers. Thanks, it’s very interesting, but i still wonder something about the logic structure of my website.

> For example, i’d like to develop a forum. A moderator can delete a post. When he deletes it: the user’s number of messages is decreased, like the topic one; the website sends a message to the post author to say him he’s not cool, etc. There is an action in my post controller to do that, right?

> Next, the moderator can delete a topic; for each post, i would have to do the same thing than above.

> How can i do? For example, where/when do i send the mail to the messages authors? Logically, i would like to call the deletePostAction in my deleteTopicAction, but I think it’s not really possible and good choice. …

So, to answer this, first we need to take a step back and examine some design choices. We have a couple different possible actions going on here:

1. Delete a post, which:
a) decrements the affected user’s post count, and
b) sends him an email telling him why action was taken
2. Delete a topic, which is essentially looping over every post in the topic and performing the actions of step 1, then removing the post itself.

It’s tempting to think about calling one controller action from another controller action — repeatedly calling `deletePostAction` from `deleteTopicAction` for every post in the topic, for example — but let me warn you: this way lies madness. Remember the purpose of a controller: to handle a request and return a response. Calling one controller action from another doesn’t really make sense because by the end of the request, you can only return one single response to the user, so all the responses from the other actions that you called are either lost (along with any important information that they might have included), or have to be hacked into your main response somehow… something that can and _will_ get very ugly, very quickly.

However, calling an [embedded controller](http://symfony.com/doc/current/book/templating.html#embedding-controllers) from a view (which, by the way, creates a separate sub-request entirely) _is_ possible and even encouraged. Since you’re dealing with fully rendered templates at that point, it’s much easier to include relevant information from the sub-request into your main request without trying to defy the laws of physics.

I guess a good way to think about the purpose of a controller action might be that it receives a request, triggers any application logic necessary to handle that request, then returns a response with the results of that application logic. The keyword is “triggers,” meaning that the action doesn’t have to handle the logic itself, it just has to delegate to some code that can. A service, for example.

Thinking in that context about today’s question, then yes: there is a controller action to _trigger_ the deletion of the post, decrementing the user’s post count, and sending an email. There’s also a separate controller action to _trigger_ the deletion of a topic, which will in turn trigger the deletion of each post in that topic (you don’t want your deletion code to be dependent on your controller, but it’s probably okay to have two obviously related pieces of functionality as a post and the topic that contains it depend on each other). But you should push the actual _execution_ of those duties off onto one or more service classes.

Say you have a PostManager service that takes an `EntityManager` instance and a `SwiftMailer` instance as constructor arguments. It might have some methods that look something like this (this is just a quick example, there are obviously a few ways you could handle this, including using events as previously mentioned):

public function deletePost(Post $post, $reason)
{
    $user = $post->getAuthor();
    $user->incrementPostCount(-1);
    $this->em->persist($user);
    $this->sendDeletionEmail($reason);
    $this->em->remove($post);
    $this->em->flush();
}

protected function sendDeletionEmail($reason)
{
    // handle the creation and sending of the email using the mailer instance
}

Now your service knows how to handle all the logic behind deleting a post, no matter where the post came from. It is now decoupled from your `deletePostAction`, meaning that while the `deletePostAction` (or any other controller action, for that matter) can trigger the deletion of a post, the deletion of the post is not dependent on the controller action. You could just as easily execute the deletion code from a console command that prunes posts with low ratings, or from any other context that you can imagine.

Then to handle the `deleteTopicAction`, you might create a `TopicManager` service. I would pass the TopicManager the `PostManager` service as a constructor argument. It would similarly have a `deleteTopic()` method, but it would then be able to iterate through each post in the topic and pass each of them to the topic manager’s delete method:

public function deleteTopic(Topic $topic, $reason)
{
    $user = $topic->getCreator();
    $user->incrementTopicCount(-1);
    $this->em->persist($user);
    foreach ($topic->getPosts() as $post) {
        $this->postManager->deletePost($post, $reason);
    }
    $this->sendTopicDeletionEmail($topic, $reason);
    $this->em->remove($topic);
    $this->em->flush();
}

Obviously, there are a lot of ways to make this prettier. For one, you could make flushing after the deletion of a post optional (default to true), so that you can iterate through and delete each post without making a trip to the database each time, only flushing at the end of the `deleteTopic()` method. Also, you could consider extracting a collection of users from all the objects to be deleted, removing duplicates, and only sending the deletion email to each user once, instead of once per post they’ve made in the thread. But the point is that you’ve decoupled your application logic from your controller actions, and you’re simply using the controller actions to trigger that logic when a user requests it.

About these ads