Symfony2 consistent routing

Software consistency in large teams is very important, because nobody likes whitespace or formatting commits or arguments about on which line brackets should be placed. Fortunately Symfony2 has a nice Standards Document that you can follow. PHP CodeSniffer can be loaded with the Symfony2 coding standard so that everybody can see the violations on the Continuous Integration (CI) server.

Unfortunately Symfony2 is not that strict everywhere. This lack of strictness allows a group of programmers to argue over and mess up a project. One of the discussions could be about naming routes. Symfony1 had a really nice option for “default routing”, allowing you to specify a single rule called “default” that had url: “/:module/:action/*”, but unfortunately that feature is not present in Symfony2. In Symfony2 we can either define routes individually or we can generate them using the CRUD generator (see my previous post). This is not a bad option as I will show below.

Naive routing

Routing to specific actions in controllers in Symfony2 can be put in a “routing.yml” file as described in the Routing section of the Symfony2 manual.

# app/config/routing.yml
product:
    pattern:   /product/
    defaults:  { _controller: AcmeDemoBundle:Product:index }

product_edit:
    pattern:  /product/edit/{id}
    defaults: { _controller: AcmeDemoBundle:Product:edit }

It is easy to understand that this way the file becomes large and unless people agree on a very strict naming policy it needs to be consulted every time you want to make a link from a view or controller to any action.

CRUD generated routing

After using the CRUD generator to generate scaffolding for an Entity called “Product”,

maurits@pc:~/project$ app/console generate:doctrine:entity --entity=AcmeDemoBundle:Product
...
Configuration format (yml, xml, php, or annotation) [annotation]:
...
maurits@pc:~/project$ app/console generate:doctrine:crud --entity=AcmeDemoBundle:Product
...
Do you want to generate the "write" actions [no]? yes
...
Configuration format (yml, xml, php, or annotation) [annotation]:
...
Routes prefix [/product]:
...

I edited my routing to include this rule:

# app/config/routing.yml
demo_bundle:
    resource: "@AcmeDemoBundle/Controller/"
    type: annotation
    prefix: /

This is the only routing entry you need per bundle when using (CRUD generated) routing annotation in your controllers. The “@Route” and “@Template” annotations can be seen below:

/**
 * Product Controller
 *
 * @Route("/product")
 */
class ProductController extends Controller
{
    /**
     * Lists all Product entities.
     *
     * @Route("/", name="product")
     * @Template()
     */
    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();
        $entities = $em->getRepository('AcmeDemoBundle:Product')->findAll();
        return array('entities' => $entities);
    }

    // ...
}

Your routes will look like:

maurits@pc:~/project$ app/console router:debug
[router] Current routes
Name                                                   Method Pattern
...                                                    ...    ...
product                                                ANY    /product/
product_show                                           ANY    /product/{id}/show
product_new                                            ANY    /product/new
product_create                                         POST   /product/create
product_edit                                           ANY    /product/{id}/edit
product_update                                         POST   /product/{id}/update
product_delete                                         POST   /product/{id}/delete

Default Routing in Symfony2

But would it not be prettier when the only routing entry you needed (per bundle) was this one?

# app/config/routing.yml
demo_bundle:
    resource: "@AcmeDemoBundle"
    type: default
    prefix: /

And your code would NOT contain “@Route” and “@Template” annotations and look like:

/**
 * Product Controller
 */
class ProductController extends Controller
{
    /**
     * Lists all Product entities.
     */
    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();
        $entities = $em->getRepository('AcmeDemoBundle:Product')->findAll();
        return array('entities' => $entities);
    }

    // ...
}

Your routes could look like this:

maurits@pc:~/project$ app/console router:debug
[router] Current routes
Name                                                   Method Pattern
...                                                    ...    ...
acme_demo.product.index                                ANY    /product/index.{_format}
acme_demo.product.show                                 ANY    /product/show/{id}.{_format}
acme_demo.product.new                                  ANY    /product/new.{_format}
acme_demo.product.edit                                 ANY    /product/edit/{id}.{_format}
acme_demo.product.delete                               ANY    /product/delete/{id}.{_format}

To achieve this we needed to combine the “new” & “create” and “edit” & “update” actions and also remove
the “$request” argument from the “delete” action and replace it by a simple “$request = $this->getRequest();” call. So this code:

    /**
     * Displays a form to edit an existing Product entity.
     *
     * @Route("/{id}/edit", name="product_edit")
     * @Template()
     */
    public function editAction($id)
    {
        $em = $this->getDoctrine()->getManager();

        $entity = $em->getRepository('AcmeDemoBundle:Product')->find($id);

        if (!$entity) {
            throw $this->createNotFoundException('Unable to find Product entity.');
        }

        $editForm = $this->createForm(new ProductType(), $entity);
        $deleteForm = $this->createDeleteForm($id);

        return array(
            'entity'      => $entity,
            'edit_form'   => $editForm->createView(),
            'delete_form' => $deleteForm->createView(),
        );
    }

    /**
     * Edits an existing Product entity.
     *
     * @Route("/{id}/update", name="product_update")
     * @Method("POST")
     * @Template("AcmeDemoBundle:Product:edit.html.twig")
     */
    public function updateAction(Request $request, $id)
    {
        $em = $this->getDoctrine()->getManager();

        $entity = $em->getRepository('AcmeDemoBundle:Product')->find($id);

        if (!$entity) {
            throw $this->createNotFoundException('Unable to find Product entity.');
        }

        $deleteForm = $this->createDeleteForm($id);
        $editForm = $this->createForm(new ProductType(), $entity);
        $editForm->bind($request);

        if ($editForm->isValid()) {
            $em->persist($entity);
            $em->flush();

            return $this->redirect($this->generateUrl('product_edit', array('id' => $id)));
        }

        return array(
            'entity'      => $entity,
            'edit_form'   => $editForm->createView(),
            'delete_form' => $deleteForm->createView(),
        );
    }

has to be changed reduced into this:

    /**
     * Edits an existing Product entity.
     */
    public function editAction($id)
    {
        $em = $this->getDoctrine()->getManager();

        $entity = $em->getRepository('AcmeDemoBundle:Product')->find($id);

        if (!$entity) {
            throw $this->createNotFoundException('Unable to find Product entity.');
        }

        $editForm = $this->createForm(new ProductType(), $entity);
        $deleteForm = $this->createDeleteForm($id);

        $request = $this->getRequest();
        if ($request->getMethod() == 'POST') {

            $editForm->bind($request);

            if ($editForm->isValid()) {
                $em->persist($entity);
                $em->flush();

                return $this->redirect($this->generateUrl('edit', array('id' => $id)));
            }

        }

        return array(
            'entity' => $entity,
            'edit_form' => $editForm->createView(),
            'delete_form' => $deleteForm->createView(),
        );
    }

Relative Routing

As a bonus this bundle allows you to refer relatively to another route, so instead of writing in the view:

<!-- Acme/DemoBundle/Resources/views/Product/index.html.twig -->
<a href="{{ path('acme_demo.product.show', { 'id': entity.id }) }}">show</a>

You can simply write this:

<!-- Acme/DemoBundle/Resources/views/Product/index.html.twig -->
<a href="{{ path('show', { 'id': entity.id }) }}">show</a>

Or instead of writing this in the controller:

$this->generateUrl('acme_demo.product.show', array('id' => $id))

You can simply write this:

$this->generateUrl('show', array('id' => $id))

Since routes are now expected to be “{bundle}.{controller}.{action}” format and the current route is “acme_demo.product.index” the undefined route “show” can be automatically matched to “acme_demo.product.show”. Apart from enforcing consistency this also allows for greater reuse of (parts of) code.

Get the bundle

Now quickly go grab a copy of the LswDefaultRoutingBundle and enjoy its benefits!

 

5 thoughts on “Symfony2 consistent routing”

  1. What is the name of the routing bundle you are talking about? I can’t see it mentioned in the post :o)

    Thanks,

    Ally

  2. Hey Maurits,

    very nice! I’ve been struggling with consistency in route names myself, and I like this approach.
    Since you seem comfortable with the subject, let me ask you about one of my pet-issues with the routing system: the parameters.

    In symfony1.4 we had object routes, so we could just pass an object to a route and control the parameters from the routing file, so if I wanted to change a route from using an id to using a slug, I could do that in one place.
    In Symfony2, I always have to spell out all parameters in the templates, removing the central control, forcing me to repeat myself.
    At the same time, seeing how powerful the parameter converters work in one direction, it seems weird to me why they can’t just be used the other way as well (convert object to parameters).

    What are your thoughts on that matter?

    Daniel

  3. @Daniel: Thank you for comment. It seems that you are referring to object routing as explained here at http://symfony.com/legacy/doc/jobeet/1_2/en/05?orm=Propel#chapter_05_object_route_class. Well it seems that ParamConverter might do the job you need. It is described in http://symfony.com/doc/2.1/bundles/SensioFrameworkExtraBundle/annotations/converters.html as was pointed out in this thread: http://www.mail-archive.com/symfony-users@googlegroups.com/msg36879.html. I hope this helps you and if it does not, please explain the difference between what you want and what this provides. Kind regards, Maurits

  4. Hey Maurits,

    yeah, object routing. In sf1.4 this used to work both ways.. you just pass an object to the url_for/link_to methods, and all the parameters are automatically extracted according to the routing.yml definition. Similarly, the controllers pulled the object in one call.

    In SF2 however, only the controller work is easy. In the templates you now have to spell out every parameter to build the route. So if you wanted to change a route to use slug instead of id, you also have to update all templates referencing that route. This seems unnecessary.

    Does this make sense?

Leave a Reply

Your email address will not be published. Required fields are marked *