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!

 

Share

Symfony2 Default Routing Bundle

The LswDefaultRoutingBundle adds default routing to your Symfony2 application. Default routing adds a route naming scheme to the configured bundles. This forces consistent naming of routes and simplifies both programming and debugging. source: Github

At LeaseWeb we develop a lot of software in Symfony2 and we are proud to contribute our second Symfony2 bundle to the Symfony2 community, the LswDefaultRoutingBundle. The bundle automatically adds routes to the Symfony2 routing table that behave like the default route does in Symfony 1. It allows you to configure bundles (like ‘FosUserBundle’) to automatically add the route ‘fos_user.user.login’ with url ‘/user/login’ for the ‘loginAction’ function in the ‘UserController’ class. It is inspired by hidenorigoto’s DefaultRouteBundle, but it behaves somewhat different.

The bundle provides the following features:

  1. Default routing: Adds a route to the routing table for every public action you implement with name ‘{bundle}.{controller}.{action}’ and URL ‘{prefix}/{controller}/{action}’.
  2. Default templating: You do not have to specify a template for an action, behaves like the empty template annotation: @Template()
  3. Relative routing: Allows you to specify only a part of the route, while other parts are extracted from the current route. You can refer to route ‘fos_user.user.view’ from ‘fos_user.user.index’ by specifying the non-existent route ‘view’.

We use it for more than half a year now and it has become an essential part of our software. Check it out at:

You can get started by reading the readme documentation on GitHub.

We are working on open sourcing even more bundles, so stay tuned!

Share