Symfony2

Putting your Symfony2 controllers on a diet, part 2

Okay, so part one technically had a different name, so I can’t really claim that this is part two. Sue me. I was originally going to talk about using param converters to save even more space, but I realized that the process is too trivial for its own post, so here’s the secret: name your routing placeholder after a field on your entity, and typehint the controller to receive an object instanceof your entity. Like so:

[code language=”php”]
// FooController.php
use FooVendorBarBundleEntityFooEntity;

public function viewAction(FooEntity $foo)
{
// $foo is an instance of FooEntity, retrieved from the database by slug
}
[/code]

Voila. The default Doctrine param converter just looked up your FooEntity, found the one with a slug that matched whatever was requested, and injected it into your controller. Magic. You can also get similar free functionality by typehinting the Request object. Very handy; we’ll be doing that with our controller from now on. Keep reading after the break for more “real diet tips” for your Symfony2 controllers (I just tripped a bunch of spam filters by typing that).

Anyway, to bring you up to speed, this is the state of our hypothetical controller action that we’re trying to slim down:

[code language=”php”]
// FooVendor/BarBundle/Controller/ExampleController.php
use FooVendorBarBundleEventCommentEvent;

public function addCommentAction($post_id)
{
$em = $this->container->get(‘doctrine’)->getEntityManager();
$request = $this->container->get(‘request’);

$post = $em->getRepository(‘FooVendorBundle:Post’)->find($post_id);
$comment = new Comment();
$comment->setPost($post);
$form = $this->createForm(new CommentType(), $comment);

if (‘POST’ === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($comment);
$em->flush();

$dispatcher = $this->container->get(‘event_dispatcher’);
$dispatcher->dispatch(‘foo_bundle.post.comment_added’, new CommentEvent($post, $comment));

return $this->redirect(/* some redirect url */);
}
}

return $this->render(‘FooVendorBundle:Post:addComment.html.twig’, array(
‘form’ => $form->createView(),
));
}
[/code]

Tonight I wanted to talk about creating a service to function as a model manager. 85% of the controller actions that I write on a daily basis deal almost exclusively with manipulating a Doctrine entity in some way (or a Propel object, if you swing that way). And sometimes I need to deal with similar actions (updating an existing entity with some information and persisting it to the database, for example) across multiple controllers. If I decide to make a change, I have to propagate it (as essentially a copy-paste job) across n actions, and hope I don’t miss any. The solution to that problem is to create a service class dedicated to handling all that for you: a model manager. Let’s create a very simple one for our example controller (I’ll comment in the code as I go):

[code language=”php”]
// FooVendor/BarBundle/Entity/CommentManager.php
namespace FooVendorBarBundleEntity;

use DoctrineORMEntityManager;
use DoctrineORMEntityRepository;

class CommentManager
{
/**
* Holds the Doctrine entity manager for database interaction
* @var EntityManager
*/
protected $em;

/**
* Entity-specific repo, useful for finding entities, for example
* @var EntityRepository
*/
protected $repo;

/**
* The Fully-Qualified Class Name for our entity
* @var string
*/
protected $class;

public function __construct(EntityManager $em, $class)
{
// Even though we have three properties, we only need two constructor arguments…
$this->em = $em;
$this->class = $class;
$this->repo = $em->getRepository($class);
// … because we can find the repo using those two
}
}
[/code]

That’s the skeleton. It doesn’t do anything yet, but having access to the entity manager and the entity’s repository class give us some powerful options. Let’s make our first addition; we’ll add a createComment() method:

[code language=”php”]
// FooVendor/BarBundle/Entity/CommentManager.php

/**
* @return Comment
*/
public function createComment()
{
$class = $this->class;
$comment = new $class();

return $comment;
}
[/code]

Again, simple. Let’s implement it (and at the same time refactor to use typehinting for the Request object):

[code language=”php”]
// FooVendor/BarBundle/Controller/ExampleController.php
use FooVendorBarBundleEventCommentEvent;
use SymfonyComponentHttpFoundationRequest;

public function addCommentAction(Request $request, $post_id)
{
$em = $this->container->get(‘doctrine’)->getEntityManager();

$post = $em->getRepository(‘FooVendorBundle:Post’)->find($post_id);
$comment = $this->getCommentManager()->createComment();
$comment->setPost($post);
$form = $this->createForm(new CommentType(), $comment);

if (‘POST’ === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($comment);
$em->flush();

$dispatcher = $this->container->get(‘event_dispatcher’);
$dispatcher->dispatch(‘foo_bundle.post.comment_added’, new CommentEvent($post, $comment));

return $this->redirect(/* some redirect url */);
}
}

return $this->render(‘FooVendorBundle:Post:addComment.html.twig’, array(
‘form’ => $form->createView(),
));
}

/**
* @return CommentManager
*/
protected function getCommentManager()
{
return $this->container->get(‘foo_vendor.manager.comment’);
}
[/code]

Okay, wait. We actually just increased the amount of code we had in our controller class, by adding the getCommentManager() method. The reason I did that is to provide typehinting if you use any kind of IDE with code completion. It’s very handy, and is one of the shortcomings that I see in the dependency injection container that Symfony2 uses: retrieving a service from the container does not provide any sort of code completion. So, we add a convenience method. That’s okay. Now, let’s do something useful with our manager, instead of writing methods that just let us replace one line of code with another line of code. The biggest chunk of logic is inside the if($form->isValid()) control statement, so let’s target that. We could just make a saveComment() method that replaces the $em->persist($comment); $em->flush(); calls, but we can do one better than that. First, let’s refactor our CommentManager to take the event dispatcher as a constructor argument:

[code language=”php”]
// FooVendor/BarBundle/Entity/CommentManager.php
namespace FooVendorBarBundleEntity;

use SymfonyComponentEventDispatcherEventDispatcherInterface;
use DoctrineORMEntityManager;
use DoctrineORMEntityRepository;
use FooVendorBarBundleEventCommentEvent;

class CommentManager
{
/**
* Holds the Symfony2 event dispatcher service
*/
protected $dispatcher;

/**
* Holds the Doctrine entity manager for database interaction
* @var EntityManager
*/
protected $em;

/**
* Entity-specific repo, useful for finding entities, for example
* @var EntityRepository
*/
protected $repo;

/**
* The Fully-Qualified Class Name for our entity
* @var string
*/
protected $class;

public function __construct(EventDispatcherInterface $dispatcher, EntityManager $em, $class)
{
$this->dispatcher = $dispatcher;
$this->em = $em;
$this->class = $class;
$this->repo = $em->getRepository($class);
}

/**
* @return Comment
*/
public function createComment()
{
$class = $this->class;
$comment = new $class();

return $comment;
}
}
[/code]

Now we can create our `saveComment()` method…

[code language=”php”]
public function saveComment(Post $post, Comment $comment)
{
$comment->setPost($post);
$this->em->persist($comment);
$this->em->flush();
$this->dispatcher->dispatch(‘foo_bundle.post.comment_added’, new CommentEvent($post, $comment));
}
[/code]

… and refactor our controller:

[code language=”php”]
// FooVendor/BarBundle/Controller/ExampleController.php
use SymfonyComponentHttpFoundationRequest;

public function addCommentAction(Request $request, $post_id)
{
$em = $this->container->get(‘doctrine’)->getEntityManager();

$post = $em->getRepository(‘FooVendorBundle:Post’)->find($post_id);
$comment = $this->getCommentManager()->createComment();
$form = $this->createForm(new CommentType(), $comment);

if (‘POST’ === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$this->getCommentManager()->saveComment($post, $comment);

return $this->redirect(/* some redirect url */);
}
}

return $this->render(‘FooVendorBundle:Post:addComment.html.twig’, array(
‘form’ => $form->createView(),
));
}

/**
* @return CommentManager
*/
protected function getCommentManager()
{
return $this->container->get(‘foo_vendor.manager.comment’);
}
[/code]

Not bad, eh? Now no matter where you’re saving your comments, you can be assured that the exact same things are happening every time. Incidentally, now would be a great time to also create a PostManager service, give it a find() method. Something simple will do:

[code language=”php”]
/**
* @return Post
*/
public function find($id)
{
return $this->repo->find($id);
}
[/code]

With that, you can refine the controller even further:

[code language=”php”]
// FooVendor/BarBundle/Controller/ExampleController.php
use SymfonyComponentHttpFoundationRequest;

public function addCommentAction(Request $request, $post_id)
{
$post = $this->getPostManager()->find($post_id);
$comment = $this->getCommentManager()->createComment();
$form = $this->createForm(new CommentType(), $comment);

if (‘POST’ === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$this->getCommentManager()->saveComment($post, $comment);

return $this->redirect(/* some redirect url */);
}
}

return $this->render(‘FooVendorBundle:Post:addComment.html.twig’, array(
‘form’ => $form->createView(),
));
}
[/code]

You’ve just obviated the need for using the Doctrine EntityManager directly in the controller. That means that if somewhere down the line you switch to Propel, or MongoDB (using the Doctrine ODM), or something else entirely, you don’t have to edit umpteen million controller actions: just a couple manager services. Congrats! We’ve now trimmed our example controller action down by quite a bit, moving the business logic out of the thin controller and into services where it belongs. Doesn’t that make you happy?

Further Reading on Symfony2

27 thoughts on “Putting your Symfony2 controllers on a diet, part 2

  1. Is it possible that near the end, under “With that, you can refine the controller even further:”, you pasted the SaveComment method again, when it should have been the getPostManager method or something?

    Thanks for a great series, it’s pure gold for me. Wish I could start refactoring every controller I made so far :-)

    1. Looks like you’re right; I edited the post to (hopefully) make a little more sense. And hey, thanks for reading! If there are other topics you’d like to see posts on, let me know; I’d be happy to oblige.

      1. No worry, it was very clear.
        My pleasure, and if I have a topic in mind, I’ll let you know. Thanks for sharing!

      2. Small typo btw (just so people don’t try, fail and quit :-) )
        use SymfonyComponentHttpFoundationRequest; ->
        use SymfonyComponentHttpFoundationRequest;

        (Using this page for a brand new controller as we speak btw, so kudos)

        1. The prettyprint parser I use for syntax highlighting swallows single backslashes, so sometimes I forget to double up and this is the result. Thanks for the catch!

  2. The only thing i don’t understand is why flush is in managers. If we use call 2 different manager::save, they’ll do two flushes. It’s not what we’d want in most cases (if one of them fails, we’ll have incomplete data). Or is the rule “have only one manager per action”?

    1. I’ll write up a post on separation of concerns at some point, but I usually try to have one manager method per entity, which handles everything related to that entity: finding, saving, deleting, etc.

      There are a couple ways to avoid flushing multiple times per request (which, as you noted, is less than ideal)… one way that you might handle it is to add a $andFlush = true parameter to each manager method which might be called during the same request. Then you can explicitly tell your methods not to flush (checking against the passed param, of course) until the last method of the request.

  3. A question here:
    I’m making heavy use of a Manager class, as per your suggestion (and loving it so far).

    I’d like to add validation as a responsibility to that manager class. Since such a Manager class is responsible for the entire lifecycle of a certain kind of object, validation seems a logic step to me. Does that sound ok?

    However: I can’t seem to find the right way to inject the Validator component into the class.
    I don’t want to just inject the container (to do get->’validator’), so how do I go about it?

    Cheers!

    Dieter

      1. Certainly, that’s exactly what I want to do, but I asked the question for two reasons:
        – do you happen to know how to inject the freakin’ validator service? :D (can’t seem to find it anywhere)
        – would you agree with the idea of letting validation be a rightful responsibility of the manager object?

        Cheers!

        1. I am an incredibly stupid, stupid person sometimes.

          “- do you happen to know how to inject the freakin’ validator service? :D (can’t seem to find it anywhere)”

          -> it’s “@validator”, just as you have “@mailer”, etc.

          Apoligies :-)

          D.

          1. Glad you got that figured out! For posterity, you can use the command php app/console container:debug to dump all the public services that the container knows about, including service IDs and classnames.

  4. Thanks a lot for the quality of your post.

    I could have get around something I did not even thought being able to apply without an example. And you did it so brilliantly!

    I remarked a few things:

    I think you forgot to mention to add the new method in the controller ExampleController::getPostManager() in the controller to point to the service.
    You should give a link to the (brillant, btw) “Leveraging the Symfony2 event dispatcher” article because what you showed here is incomplete without it :)

    On an other topic:

    Thanks a lot for your articles. It’s hard to be disciplinated to maintain a blog. I understand the struggle.

    I would suggest you to have your own wiki and anything you document during work, or prepare, could be saved there as a draft. It could be your own source of blog subjects.

    I personally do this and have dozens of article that “just” needs me to anonymize, re-edit, and publish. I also use delicious with an “inspiration” tag to help on other.

    But, it all takes time.

    Cheers!

  5. Nice simplifications. There are some areas for possible improvement:

    1. Use type hinting for posts (instead of using $post_id).
    2. Inject the commentManager through annotations (with automatic_controller_injections) using JMSDiExtraBundle: http://jmsyst.com/bundles/JMSDiExtraBundle/master/configuration
    3. Return [‘form’ => $form->createView()] and use the @Template annotation from SensioFrameworkExtraBundle (somewhat a matter of taste)

    Not really a simplification, but switching to a non-Controller-based implementation together with the points above makes for a very easily (unit) testable method.

    For flushing I would just flush at the end to achieve atomicity. Java EE does support more powerful declarative transaction demarcation, but I haven’t seen a bundle for Symfony or Doctrine doing that (I’m pondering doing an implementation, though). Many apps fortunately do not need anything more complicated.

    I don’t think that adding find to the manager makes that much sense given that repositories exist, but removing the find completely here simplifies things even more.

  6. Instead of introducing another layer of compexity (the Manager), why not use the Repository instead? You could define the saveComment method in the CommentRepository. After all, that’s what the Repository classes are for: managing a certain type of entity.

    1. For simple use cases, that’s not a bad idea, but the most compelling argument I can think of for creating a custom service is adding additional dependencies. If you’re doing a lot of work surrounding the creation and persistence of entities, it’s nice to have one place that wraps it all together.

    2. If you have transactions, it will be hell to unraveled if you had saved in each repository. Imagine if the last repo failed to save, you now have to un do all the stuff you comitted to the database manually.

  7. Your article may be a year old but it’s still really interesting. I was looking to abstract some entities to be used in a bundle’s controllers and this is the ideal way to do it.

    I found this base class that could be a good example since your gist isn’t available anymore. What do you think?

    https://gist.github.com/tuongaz/2271046

  8. Loved this, helped me alot, will be applying this to my ugly controllers. thanks very muvh indeed.

  9. Going down this approach of creating a ‘manager’ service for each entity, if I had say 100 entities, then wouldn’t I need 100 ‘manager’ services which would create an unmanageable services.yml file, especially if I was trying to find a specific manager??

  10. This seems like a good strategy. The issue I run into is trying to unit test a service that injects doctrine or entity manager. I was under the impression that service classes should be tested with mock data not data from a repository for instance. But this service can’t be instantiated without injecting doctrine entity manager. So should I look at manipulating an extended PHPUnit_Framework_TestCase class in order to gain access to doctrine repository? Not sure how to proceed. Thanks for the post.

  11. is this even possible? In services.yml

    arguments:
    —-> class: FooVendorBarBundleEntityComment —> this will throw an error if key “class” exists

Leave a Reply