Symfony: 6 – Routing

Routing

A route is a map from a URL path to a controller. The file is usually app/config/routing.yml, but can be configured to be anything (including an XML or PHP file). You can also define routes using annotations. For example, suppose you want to match any URL like /blog/my-post or /blog/all-about-symfony and send it to a controller that can look up and render that blog entry. The route is simple:

# app/config/routing.yml
blog_show:
  path: /blog/{slug}
  defaults: { _controller: TestBlogBundle:Blog:show }

The path will not match simply /blog. That’s because, by default, all placeholders are required. This can be changed by adding a placeholder value to the defaults array. The path defined by the blog_show route acts like /blog/* where the wildcard is given the name slug. For the URL /blog/my-blog-post, the slug variable gets a value of my-blog-post, which is available for you to use in your controller. The _controller parameter is a special key that tells Symfony which controller should be executed when a URL matches this route. The _controller string is called the logical name. It follows a pattern that points to a specific PHP class and method. When you visit /blog/my-post, the showAction controller will be executed and the $slug variable will be equal to my-post:

namespace Test\BlogBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class BlogController extends Controller
{
  public function showAction($slug)
  {
    // use the $slug variable to query the database
    $blog = ...;
 
    return $this->render('AcmeBlogBundle:Blog:show.html.twig', array(
      'blog' => $blog,
    ));
  }
}

To display a list of all the available blog posts for this imaginary blog application:

blog:
  path: /blog
  defaults: { _controller: TestBlogBundle:Blog:index }

This route is as simple as possible – it contains no placeholders and will only match the exact URL /blog. But what if you need this route to support pagination, where /blog/2 displays the second page of blog entries? Update the route to have a new {page} placeholder. The value matching {page} will be available inside your controller:

blog:
  path: /blog/{page}
  defaults: { _controller: TestBlogBundle:Blog:index }

Since placeholders are required by default, this route will no longer match on simply /blog. Instead, to see page 1 of the blog, you’d need to use the URL /blog/1! Modify the route to make the {page} parameter optional. This is done by including it in the defaults collection:

blog:
  path: /blog/{page}
  defaults: { _controller: TestBlogBundle:Blog:index, page: 1 }

Note that routes with optional parameters at the end will not match on requests with a trailing slash (i.e. /blog/ will not match, /blog will match). In reality, the entire defaults collection is merged with the parameter values to form a single array even $_format, $_controller and $_route are available.

The Symfony router will always choose the first matching route it finds. Regular expression requirements can easily be added for each parameter. The \d+ requirement is a regular expression that says that the value of the {page} parameter must be a digit (i.e. a number):

blog:
  path: /blog/{page}
  defaults: { _controller: TestBlogBundle:Blog:index, page: 1 }
  requirements:
    page: \d+

For incoming requests, the {culture} portion of the URL is matched against the regular expression (en|fr):

homepage:
  path: /{culture}
  defaults: { _controller: TestDemoBundle:Main:homepage, culture: en }
  requirements:
    culture: en|fr

In addition to the URL, you can also match on the method of the incoming request (i.e. GET, HEAD, POST, PUT, DELETE). Suppose you have a contact form with two controllers – one for displaying the form (on a GET request) and one for processing the form when it’s submitted (on a POST request). This can be accomplished with the following route configuration:

contact:
  path: /contact
  defaults: { _controller: TestDemoBundle:Main:contact }
  methods: [GET]
 
contact_process:
  path: /contact
  defaults: { _controller: TestDemoBundle:Main:contactProcess }
  methods: [POST]

The routing system can be extended to have an almost infinite flexibility using conditions:

contact:
  path: /contact
  defaults: { _controller: TestDemoBundle:Main:contact }
  condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"

You can do any complex logic you need in the expression by leveraging two variables that are passed into the expression:

  • context: An instance of RequestContext1, which holds the most fundamental information about the route being matched
  • request: The Symfony Request object

The following is an example of just how flexible the routing system can be:

article_show:
  path: /articles/{culture}/{year}/{title}.{_format}
  defaults: { _controller: TestDemoBundle:Article:show, _format: html }
  requirements:
    culture: en|fr
    _format: html|rss
    year: \d+

As you’ve seen, this route will only match if the {culture} portion of the URL is either en or fr and if the {year} is a number. This route also shows how you can use a dot between placeholders instead of a slash. When using the special _format parameter, the matched value becomes the “request format” of the Request object. It can also be used in the controller
to render a different template for each value of _format. The _format parameter is a very powerful way to render the same content in different formats.

Additionally, there are three parameters that are special: each adds a unique piece of functionality inside your application:

  • _controller: As you’ve seen, this parameter is used to determine which controller is executed when the route is matched
  • _format: Used to set the request format
  • _locale: Used to set the locale on the request. The value will also be stored on the session so that subsequent requests keep this same locale

All routes are loaded via a single configuration file – usually app/config/routing.yml. Commonly, however, you’ll want to load routes from other places, like a routing file that lives inside a bundle. This can be done by “importing” that file:

# app/config/routing.yml
test_hello:
  resource: "@TestHelloBundle/Resources/config/routing.yml"

You can also choose to provide a “prefix” for the imported routes. For example, suppose you want the test_hello route to have a final path of /admin/hello/{name} instead of simply /hello/{name}:

# app/config/routing.yml
test_hello:
  resource: "@TestHelloBundle/Resources/config/routing.yml"
  prefix: /admin

While adding and customizing routes, it’s helpful to be able to visualize and get detailed information about your routes. A great way to see every route in your application is via the router:debug console command. Execute the command by running the following from the root of your project:

$ php app/console router:debug
 
// To get very specific information on a single route:
$ php app/console router:debug article_show
 
// Rest whether a URL matches a given route:
php app/console router:match /blog/my-latest-post

The routing system should also be used to generate URLs. In controllers that extend Symfony’s base Controller, you can use the generateUrl() method, which call’s the router service’s generate() method. To generate a URL, you need to specify the name of the route (e.g. blog_show) and any wildcards (e.g. slug = my-blog-post) used in the path for that route. With this information, any URL can easily be generated:

class MainController extends Controller
{
  public function showAction($slug)
  {
    // ...
 
    $url = $this->generateUrl(
      'blog_show',
      array('slug' => 'my-blog-post')
    );
  }
}

If the frontend of your application uses Ajax requests, you might want to be able to generate URLs in JavaScript based on your routing configuration. By using the FOSJsRoutingBundle, you can do exactly that:

var url = Routing.generate(
  'blog_show',
  {"slug": 'my-blog-post'}
);

By default, the router will generate relative URLs (e.g. /blog). To generate an absolute URL, simply pass true to the third argument of the generate() method:

$this->get('router')->generate('blog_show', array('slug' => 'my-blog-post'), true);
// http://www.example.com/blog/my-blog-post

When generating absolute URLs for scripts run from the command line, you’ll need to manually set the desired host on the RequestContext object:

$this->get('router')->getContext()->setHost('www.example.com');

The generate method takes an array of wildcard values to generate the URI. But if you pass extra ones, they will be added to the URI as a query string:

$this->get('router')->generate('blog', array('page' => 2, 'category' => 'Symfony'));
// /blog/2?category=Symfony

The most common place to generate a URL is from within a template when linking between pages in your application. This is done just as before, but using a template helper function:

<a href="{{ path('blog_show', {'slug': 'my-blog-post'}) }}">
  Read this blog post.
</a>
 
<a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}">
  Read this blog post.
</a>

Leave a Reply