The Ultimate Developer Guide to Symfony - Routing 17/02/2016
Reference: This article is intended to be as complete as possible and is kept up to date.
TL;DR:
$parameters = $urlMatcher->match($request->getPathInfo()); $request->attributes->add(array('_controller' => $parameters['_controller']); $request->attributes->add(array('_route' => $parameters['_route']); unset($parameters['_controller'], $parameters['_route']); $request->attributes->add(array('_route_params' => $parameters);
In this guide we explore the standalone libraries (also known as "Components") provided by Symfony to help us build applications.
We've already seen:
We're now about to check Routing and YAML, then in the next articles we'll have a look at:
We'll also see how HttpKernel enables reusable code with Bundles, and the different ways to organize our application tree directory.
Finally we'll finish by putting all this knowledge in practice by creating a "fortune" project with:
- an endpoint that allows us to submit new fortunes
- a page that lists all fortunes
- a command that prints the last fortune
Routing
Symfony provides a Routing component which allows us, for a HTTP request/URL, to execute a specific function (also known as "Controller").
Note: Controllers must be a callable, for example:
- an anonymous function:
$controller = function (Request $request) { return new Response() };
.- an array with an instance of a class and a method name:
$controller = array($controller, 'searchArticles');
.- a fully qualified classname with a static method name:
$controller = 'Vendor\Project\Controller\ArticleController::searchArticles'
.Controllers can take a Request argument and should return a Response instance.
It revolves around the following interface:
<?php
namespace Symfony\Component\Routing\Matcher;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
interface UrlMatcherInterface
{
/**
* @param string $pathinfo
*
* @return array Route parameters (also contains `_route`)
*
* @throws ResourceNotFoundException
* @throws MethodNotAllowedException
*/
public function match($pathinfo);
}
Note: For brevity the interface has been stripped from
RequestContextAwareInterface
.
In actual applications we don't need to implement it as the component provides
a nice implementation that works with RouteCollection
:
<?php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
$collection = new RouteCollection();
$collection->add('search_articles', new Route('/v1/articles', array(
'_controller' => 'Vendor\Project\Controller\ArticleController::search',
), array(), array(), '', array(), array('GET', 'HEAD')));
$collection->add('edit_article', new Route('/v1/articles/{id}', array(
'_controller' => 'Vendor\Project\Controller\ArticleController::edit',
), array(), array(), '', array(), array('PUT')));
RouteCollection
allows us to configure which Request will match our controllers:
via URL patterns and Request method. It also allows us to specify parts of the URLs
as URI parameters (e.g. id
in the above snippet).
Building route configuration by interacting with PHP code can be tedious, so the Routing component supports alternative configuration formats: annotations, XML, YAML, etc.
Tip: have a look at
Symfony\Component\Routing\Loader\YamlFileLoader
.
YAML
Symfony provides a YAML component which allows us to convert YAML configuration into PHP arrays (and vice versa).
For example the following YAML file:
# /tmp/routing.yml
search_articles:
path: /api/articles
defaults:
_controller: 'Vendor\Project\Controller\ArticleController::search'
methods:
- GET
- HEAD
edit_article:
path: '/api/articles/{id}'
defaults:
_controller: 'Vendor\Project\Controller\ArticleController::edit'
methods:
- PUT
Note: Some string values must be escaped using single quotes because the YAML has a list of reserved characters, including:
@
,%
,\
,-
,:
[
,]
,{
and}
.
Can be converted using:
<?php
use Symfony\Component\Yaml\Yaml;
$routing = Yaml::parse(file_get_contents('/tmp/routing.yml'));
This will result in the equivalent of the following array:
<?php
$routing = array(
'search_articles' => array(
'path' => '/api/articles',
'defaults' => array(
'_controller' => 'Vendor\Project\Controller\ArticleController::search',
),
'methods' => array(
'GET',
'HEAD',
),
),
'edit_article' => array(
'path' => '/api/articles/{id}',
'defaults' => array(
'_controller' => 'Vendor\Project\Controller\ArticleController::edit',
),
'methods' => array(
'PUT',
),
),
);
Note: the Routing component uses another component to then build
RouteCollection
from this array: the Config component which is out of the scope of this guide.
There's also $yaml = Yaml::dump($array);
that converts a PHP array into a YAML
string.
Conclusion
The Routing component allows us to define which Controllers should be executed for the given Request, and the Yaml component allows us to configure it in a simple way.
HttpKernel provides a RouterListener
which makes use of UrlMatcher
when the
Request is received to find a corresponding controller.
Note:
Request->attributes
is used to store information about the current Request such as the matched route, the controller, etc. It's used internally by Symfony but we could also store our own values in it.
Some might be concerned with performance: reading the configuration from the filesystem may slow down the application.
Don't panic! There's a PhpMatcherDumper
class which can generate an implementation
of UrlMatcherInterface
with all configuration in an optimized way. It might look
like this:
<?php
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;
class appDevUrlMatcher extends Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher
{
public function __construct(RequestContext $context)
{
$this->context = $context;
}
public function match($pathinfo)
{
$allow = array();
$pathinfo = rawurldecode($pathinfo);
$context = $this->context;
// edit_article
if (preg_match('#^/v1/articles/(?P<id>[^/]++)$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'PUT') {
$allow[] = 'PUT';
goto not_edit_article;
}
return $this->mergeDefaults(array_replace($matches, array('_route' => 'edit_article')), array ( '_controller' => 'Vendor\Project\Controller\ArticleController::edit',));
}
not_edit_article:
// search_articles
if ($pathinfo === '/v1/articles') {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_search_articles;
}
return array ( '_controller' => 'app.article_controller:search', '_route' => 'Vendor\Project\Controller\ArticleController::search',);
}
not_search_articles:
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
}
}