PHP
Article
By Nicolas Scolari

Getting Started with Symfony2 Route Annotations

By Nicolas Scolari
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

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() { /* ... */ }
}
--ADVERTISEMENT--

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?

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.Is it good?