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 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):

[code lang=php]
public function deletePost(Post $post, $reason)
$user = $post->getAuthor();

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:

[code lang=php]
public function deleteTopic(Topic $topic, $reason)
$user = $topic->getCreator();
foreach ($topic->getPosts() as $post) {
$this->postManager->deletePost($post, $reason);
$this->sendTopicDeletionEmail($topic, $reason);

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.

Further Reading on Symfony2

6 thoughts on “Symfony2 controllers and application logic, revisited

  1. A different take on this would be to use lifecycle callbacks on models. You can tell a model to call a special action when its being created / updated / deleted etc … so whenever a post is deleted it could call a function that would do:

    This way nothing even appears in the controllers related to this. All you do in the controller is delete one post or all the post in a topic.

    Same way, using cascade delete on the topic would mean you’d only have to call deleteTopic and the deletion of related posts would happen behind the scenes.

    1. Great point! That would be even cleaner than the approach I suggested, although you’d probably still want a separate service to handle the sending of deletion emails, because that doesn’t seem like something the entity should be responsible for. Although, you could listen to Doctrine’s preRemove lifecycle event and handle the sending of emails that way, too.

    2. “Same way, using cascade delete on the topic would mean you’d only have to call deleteTopic and the deletion of related posts would happen behind the scenes.”

      That’s ok, but only deletion in database, no? If you just use doctrine cascade, does Symfony call the related deleteAction automatically?

      1. deleteAction (implying: a controller action) would certainly not be called. The question is to know if Doctrine would call the preRemove lifecycle event on all objects in this case (through cascade). I do believe so, but better make a test for this.

Leave a Reply