PHP
Article

Getting Started with Symfony2 Route Annotations

By Nicolas Scolari

When you download the Standard Symfony 2 Distribution, it includes an interesting bundle named SensioFrameworkExtraBundle which implements a lot of great stuff, especially the opportunity to use annotations directly within your controllers.

The idea behind this article is not to convice developers to embrace this way of doing, but to point the finger at an alternative method to easily configure controllers. Keep in mind that there is no magic recipe, it depends on what you need in each specific scenario.

Symfony 2 implements a strong built-in component to manage all the routes of an application: the Routing Component. Basically, a route maps a URL to a controller action. Because Symfony is intended to be modular, a file is dedicated to this: routing.yml. You will find it in app > config > routing.yml.

In order to understand how to define our routes with annotations, I’ll take as example a simple blog application.

Step 1: creating the homepage route

We just want to link the path / to an action of our HomeController.

Without annotations

In app/config/routing.yml:

blog_front_homepage:
  path : /
  defaults:  { _controller: BlogFrontBundle:Home:index }

In src/Blog/FrontBundle/Controller/HomeController.php:

<?php
namespace Blog\FrontBundle\Controller;

class HomeController
{
    public function indexAction()
    {
        //... create and return a Response object
    } 
}

Let’s take a look at those files. In routing.yml, we declared a simple configuration for the route named blog_front_homepage with 2 parameters: the path and the action of the controller we want to target. As far as the controller goes, it doesn’t need anything special.

With annotations

In app/config/routing.yml:

blog_front:
    resource: "@BlogFrontBundle/Controller/"
    type:     annotation
    prefix:   /

In src/Blog/FrontBundle/Controller/HomeController.php:

<?php

namespace Blog\FrontBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class HomeController
{
    /**
     * @Route("/", name="blog_home_index")
     */
    public function indexAction() { /* ... */ }
}

First, let’s see what happened to routing.yml:

  • resource targets the controller to impact
  • type obviously defines the way we declare routes
  • prefix defines a prefix for all actions of a controller class (optional)

What’s more interesting is how our controller is now built. Before everything else, we have to call the relevant class of the SensioFrameworkExtraBundle: use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;. We are now ready to implement the route and the parameters we want to assign: in this case only the path and the name (we will later see all that we can do): @Route("/", name="blog_homepage").

Some of you might be thinking: “Well, we know how to override the controller with the routing layer, so what? At the end of the day, more code is needed for basically the same result”. And you would be right… At least for now.

Step 2: adding an article page route

Without annotations

In app/config/routing.yml:

blog_front_homepage:
  path : /
  defaults:  { _controller: BlogFrontBundle:Home:index }

blog_front_article:
  path : /article/{slug}
  defaults:  { _controller: BlogFrontBundle:Home:showArticle }

In src/Blog/FrontBundle/Controller/HomeController.php:

<?php
// namespace & uses...

class HomeController
{
    public function indexAction() { /* ... */ }
    
    public function showArticleAction($slug) { /* ... */ }
}

With annotations

In app/config/routing.yml:

blog_front:
    resource: "@BlogFrontBundle/Controller/"
    type:     annotation
    prefix:   /

In src/Blog/FrontBundle/Controller/HomeController.php:

<?php
// namespace & uses...

class HomeController
{
    /**
     * @Route("/", name="blog_home_index")
     */
    public function indexAction() { /* ... */ }
    
    /**
     * @Route("/article/{slug}", name="blog_home_show_article")
     */
    public function showArticleAction($slug) { /* ... */ }
}

Have you noticed? routing.yml doesn’t need any changes. Now, you can check from a single glance which action is being called from a route pattern.

If you want all your actions from the controller to have a prefix, for instance /admin, you can remove prefix key from routing.yml and add an extra @Route annotation at the top of your class:

In app/config/routing.yml:

blog_front: ...
 
blog_admin:
    resource: "@BlogAdminBundle/Controller/"
    type:     annotation

In src/Blog/AdminBundle/Controller/AdminController.php:

<?php
// namespace & uses...

/**
 * @Route("/admin")
 */
class AdminController
{
    /**
     * @Route("/", name="blog_admin_index")
     */
    public function indexAction() { /* ... */ }
    
    /**
     * @Route("/create", name="blog_admin_create_article")
     */
    public function createArticleAction() { /* ... */ }
}

Step 3: extra routing configuration

Setting URL default parameters

Syntax: defaults = { "key" = "value" }.

/**
 * @Route(
 *       path     = "/article/{slug}",
 *       name     = "blog_home_show_article",
 *       defaults = { "slug" = "hello" }
 * )
 */

By adding slug to the defaults key, the {slug} placeholder is no longer required. The URL /article will match this route and the value of the slug parameter will be set to hello. The URL /blog/world will also match, giving the page parameter a value of world.

Adding requirements

Syntax: requirements = { "key" = "value" }.

/**
 * @Route(
 *       path         = "/article/{slug}",
 *       name         = "blog_home_show_article",
 *       defaults     = { "slug" = "hello" },
 *       requirements = { "slug" = "[a-z]+" }
 * )
 */

We can define requirements for the slug key with a regular expression, to explicitly define that the value of {slug} has to be exclusively made of alpha characters. In the following example, we do the exact same thing with digits:

/**
 * @Route(
 *       path         = "article/{id}",
 *       name         = "blog_home_show_article",
 *       defaults     = { "id" = 1 },
 *       requirements = { "id" = "\d+" } 
 * )
 */

If you need more information about regular expressions follow this link.

Enforcing the HTTP method

Syntax: methods = { "request method" }.

/**
 * @Route(
 *       path         = "/article/{slug}",
 *       name         = "blog_home_show_article",
 *       defaults     = { "slug" = "hello" },
 *       requirements = { "slug" = "[a-z]+" },
 *       methods      = { "GET", "POST" }
 * )
 */

We can also match on the method of the incoming request (i.e. GET, POST, PUT, DELETE). Remember that if no method is specified, the route will match any of them.

Enforcing the HTTP scheme

Syntax: schemes = { "protocol" }.

/**
 * @Route(
 *       path         = "/article/{slug}",
 *       name         = "blog_home_show_article",
 *       defaults     = { "slug" = "hello" },
 *       requirements = { "slug" = "[a-z]+" },
 *       methods      = { "GET", "POST" },
 *       schemes      = { "https" }  
 * )
 */

In this case, we want to secure the route to be sure that it is accessed via the HTTPS protocol.

Enforcing the host name

Syntax: host = "myhost.com".

/**
 * @Route(
 *       path         = "/article/{slug}",
 *       name         = "blog_home_show_article",
 *       defaults     = { "slug" = "hello" },
 *       requirements = { "slug" = "[a-z]+" },
 *       methods      = { "GET", "POST" },
 *       schemes      = { "https" },
 *       host         = "myblog.com"
 * )
 */

We can also match on the HTTP host. This one will match only if the host is myblog.com.

Step 4: exercise

As we are now able to build a solid routing structure, imagine that we have to create the action to delete an article with the right route in our AdminController.php. We have to:

  • Define the path as /admin/delete/article/{id};
  • Define the name as blog_admin_delete_article;
  • Define the requirement for the key id as only digits;
  • Define the GET request method.

Stuck? Have a look at the solution below:

<?php
// src/Blog/AdminBundle/Controller/AdminController.php
// namespace & uses...

/**
 * @Route("/admin")
 */
class AdminController
{
    /* ... */

    /**
     * @Route("
     *    path          = "/delete/article/{id}", 
     *    name          = "blog_admin_delete_article"
     *    requirements  = { "id" = "\d+" },
     *    methods       = { "GET" }
     * )
     */
    public function deleteArticleAction($id) { /* ... */ }
}

Final thoughts

As you can see, managing routes with annotations is both easy to write, and easy to read and maintain. It also has the nice pro of gathering both the code and the configuration in a unique file: the controller class.

Do you use annotations, or the standard configuration? Which do you prefer, and why?

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • DanBangWTFRajib

    In PHP Annotatiosn are the comment (yes, docblock is also comment) that acts like code that executes at runtime. Controlling logical flow in the runtime via Docblocks is yuck. That’s what I think about annotations.

    The last example says it all, instead of writing my logic in method, if I start writing lines of comment without proper IDE typehinting-support is just PITA.

    Annotations/Attributes are great to change the logic at runtime in other languages, but poorly implemented in PHP.

    Not sure why symfony is pushing towards docblocks/annotations these days, given that YAML support was their home brewed first class citizen few years ago.

  • http://r.je Tom Butler
  • http://spiechu.pl/ Śpiechu

    I would allow annotations only in place where they were born – as Doctrine2 constraints. But personally in my entities I have associative array where I hold validation closures, one per field. Closure returns true or ValidationException with message what happened wrong.
    This allows extensive closure unit testing, because it can be passed around just like that.

  • Taylor Ren

    I don’t use annotations to make routes. YML is quite straightforward and the routes can be “wrapped” in separate bundles for easier management.

    There must be a reason for a known alternatives being “unknown”.

  • Fabien LARTIGUE

    Hello.
    Just for a mistake in your interesting article :
    In the code at “Step4″,
    You add ” after @Route(

    Thank you.

  • Ivan G

    I agree with every Tom’s article argument and I would add one more:
    – I do not like magic anywhere. Things should be simple, easy to follow and clear.

    Mix your controllers logic with an extra logic *in the comments* makes me feel disrupted.
    Controllers, views, resources, repositories, etc have their own place, why not let Routes be where they were designed to be?

  • Richard

    Well in my beginning with Symfony, I was like why the heck should I write every link in yml… thus prefer annotations. But yeah, it can be risky at times without much support from the IDE

  • Canguro Jack

    Very nice documentation on the Symfony Routing.

  • http://www.facebook.com/sudhir600 Sudhir Gupta

    i am still using annotation. its easy, so easy.
    but i want to explore routing.yml

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.