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