Similar to what we did for services, this article will briefly cover every possible configuration field in a Symfony routing file. We’ve broken things down into four categories: Basic Routing, Advanced Routing, Route Loading, and “The Weird Ones”
Most of this information is available in the Symfony Routing documentation, but there’s a few obscure/non-obvious things I wasn’t able to find anywhere else.
Basic Routing
To start, we’ll cover the basic building blocks of a route. These configuration fields are the ones you’ll use most day-to-day when designing your system’s URLs.
path
The path
configuration
route_name:
path: /the/url/to/match/{id}
allows you to create a pattern for your route. If the request’s URL matches your path, then Symfony will use this route’s information to handle the request. Your path can use “wildcard placeholders” ({id}
above). The path
configuration is central to Symfony’s routing system, and is covered throughout the Symfony routing docs.
controller
route_name:
controller: Some\Class\Or\Service\Name::actionMethod
allows you specify the PHP class and method Symfony should instantiate and execute for this route. This can be a class name with a method, or a service name with a method.
host
route_name:
path: /foo
host: {subdomain}.example.com
is similar to the path
configuration, in that it’s a pattern Symfony will try to match against the URL. Host, however, will match against the actual domain name of the URL. The above example would match any of the following
http://store.example.com/foo
http://admin.example.com/foo
http://foo.example.com/foo
http://etc.example.com/foo
schemes
route_name:
scheme: [https, http]
allows you to match your route against the scheme of the URL. One use of this configuration would be ensuing a particular page is only accessible via a secure, (HTTPS), URL.
methods
route_name:
methods: [POST, PUT]
allows you to restrict your routes to certain HTTP request methods. Narrowing which HTTP request methods a URL responds to can help prevent unintended side effects in your application. The classic example is a URL that performs a delete.
http://store.example.com/foo/delete/123
Pretend this URL deletes a foo
object with an ID of 123
. By restricting this URL to the POST
method, you avoid the chance someone (or some automated spider/bot) will accidentally request the URL and delete your object.
Advanced Routing
The following three configurations are still related to core routing functionality, but they go beyond the basics.
requirements
The requirements
configuration allows you to create rules for what a named section of your paths may or may not contain. Consider this configuration
route_name:
path: /foo/{id}
In the path /foo/{id}
, {id}
is a named placeholder. This path
will match URLs that look like the following
/foo/1
/foo/345
/foo/234
/foo/abc123
The requirments
field allows you to configure a regular expression that the {id}
placeholder (or any named placeholder) must match.
So, with the same path, but a requirments
field that uses a “match only digits” regular expression.
route_name:
path: /foo/{id}
requirments: '\d+'
The full route would only match URLs that looked like this
/foo/1
/foo/345
/foo/234
This route would not match the /foo/abc123
path because abc123
does not match the regular expression \d+
.
condition
The condition
configuration is where you turn when none of the other fields do exactly what you want.
route_name:
path: /some-path
condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
The value of the condition
key is code. With condition
, you can write a small program that will evaluate true
or false
— true means your rule matches, false
means it does not.
The condition language is based on expressions in Symfony’s twig templating language, but it’s enough of its own thing that Symfony has specific documentation for The Expression Syntax.
For those of you interested in DSL implementations, or are wondering what objects are available to you in the condition
code, this section of Symfony’s internals may be of interest, (hat tip to yiyi over on StackOverflow)
#File: vendor/symfony/routing/Matcher/UrlMatcher.php
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) {
return [self::REQUIREMENT_MISMATCH, null];
}
return [self::REQUIREMENT_MATCH, null];
}
Route Loading
Alright! We’ve seen a bunch of configuration keys that are all about creating your routes. The next set of keys are about organizing your routes. So far in this series we’ve always used a single routes.yaml
file for the one or two routes we’ve needed. A real world application might have dozens, hundreds, or even thousands of of different routes. Symfony offers you a number of tools for managing a large number of routes across multiple files.
resource
Managing routes across multiple files starts with the resource
configuration key.
sylius_shop:
# vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/Resources/config/routing.yml
resource: "@SyliusShopBundle/Resources/config/routing.yml"
#resource: "../relative/path/to/some/routing.yml"
The resource
field’s value is a path to another configuration file, which contains more routes. The @SyliusShopBundle
above tells Symfony to look for this file in the SyliusShopBundle bundle. Bundles are Symfony’s module/plugin system, and are how Symfony allows developers to add code to the system. Your resource path doesn’t need to be a bundle, but the @BundleName
syntax is a common thing in route configuration files.
By default, using resources is a lot like using PHP’s include
or require
statement — Symfony will just load up all the routes in the external file. There are, however, a number of configuration keys which can change the behavior of this importing.
prefix
The first resource adjacent configuration key we’ll look at is prefix
#File: routes.yaml
unique_id:
resource: "path/to/some/routing/file.yml"
prefix: /the-prefix
#File: path/to/some/routing/file.yml
route_name:
path: /foo
controller: Some\Controller\ClassFile::actionMethod
The prefix
configuration tells Symfony
Hey — that route file you’re about to load via
resource
? Everypath
in that file needs to start with the value of thisprefix
key.
For example, the configuration above will match match a URL that looks like this
http://sylius.example.com/the-prefix/foo
but will not match a URL that looks like this
http://sylius.example.com/foo
The prefix
value changes how resource
works and how the path/to/some/routing/file.yml
is interpreted. The prefix
value isn’t the only configuration field that can change the behavior of a rout configuration file. When you’re debugging routes it’s important to know how a particular routing configuration file is used.
name_prefix
The name_prefix
configuration is similar to the prefix
configuration
#File: routes.yaml
unique_id:
resource: "path/to/some/routing/file.yml"
name-prefix: science
#File: path/to/some/routing/file.yml
route_name:
path: /foo
controller: Some\Controller\ClassFile::actionMethod
This is another configuration field that controls how Symfony will interpret the routes it’s loading. In the about example, Symfony names the route in file.xml
as science_route_name
instead of simply route_name
.
Remember, all those route names need to be unique — by prefixing routes you’re importing you can often resolve naming conflicts in third party route configuration files that you don’t own.
type
The type
configuration is another configuration field that works with resource
. We’ll borrow the configuration sample from the official docs.
# config/routes.yaml
app_file:
# loads routes from the given routing file stored in some bundle
resource: '@AcmeOtherBundle/Resources/config/routing.yaml'
app_annotations:
# loads routes from the PHP annotations of the controllers found in that directory
resource: '../src/Controller/'
type: annotation
app_directory:
# loads routes from the YAML, XML or PHP files found in that directory
resource: '../legacy/routing/'
type: directory
app_bundle:
# loads routes from the YAML, XML or PHP files found in some bundle directory
resource: '@AcmeOtherBundle/Resources/config/routing/'
type: directory
By default, Symfony expects resource
to contain a path to a configuration file. However, if you set your type
to directory, you can load every configuration file in that folder into your application.
Similarly, if you’re using route annotations instead of configuration files, setting type to annotation
will allow you to point to a directly with annotated PHP class files.
trailing_slash_on_root
The trailing_slash_on_root
configuration is a hyper-specific one for a small but important edge case in how Symfony imports routes. Consider the following configuration.
#File: routes.yaml
unique_id:
resource: "path/to/some/routing/file.yml"
prefix: /the-prefix
#File: path/to/some/routing/file.yml
route_name:
path: /
controller: Some\Controller\ClassFile::actionMethod
route_name:
With the above, Symfony would respond to a URL that looks like the following
http://symfony.example.com/the-prefix/
Before trailing_slash_on_root
, there was not a way to make the same URL without the trailing slash.
http://symfony.example.com/the-prefix
For folks interested in URL semantics (and SEO) this was a small, but real, problem.
Symfony introduced the trailing_slash_on_root
option as a way to let users create a route that responds to either URL, but have the canonical URL be one with, or without, the trailing slash.
#File: routes.yaml
unique_id:
resource: "path/to/some/routing/file.yml"
prefix: /the-prefix
trailing_slash_on_root: false
#File: path/to/some/routing/file.yml
route_name:
path: /
controller: Some\Controller\ClassFile::actionMethod
route_name:
Options, Defaults, and the Route Object
There’s just a few more configuration fields to go. The next two (or six, depending on how you count) are going to require a bit of PHP background. These are the weird ones.
Symfony’s routing system is a Domain Specific Language (DSL). A DSL is (from one point of view) a simplified programming language that’s specifically designed to do one thing. Ideally, that language’s design lets the end user live in complete ignorance of the underlying implementation — you just configure the fields you need, sprinkle in a little logic if the DSL allows, and you’re good to go.
Unfortunately, while you can use the options
and defaults
fields without understanding the underlying PHP objects that Symfony is using, these configuration fields don’t make much sense unless you look at that underlying PHP code — specifically the route object.
Behind the scenes, for each individual route you configure, Symfony will instantiate a Symfony\Component\Routing\Route
object.
#File: vendor/symfony/routing/Route.php
/*...*/
namespace Symfony\Component\Routing;
/*...*/
class Route implements \Serializable
{
/*...*/
}
Your configuration will determine how that Route object behaves. The following route configuration fields will set certain state/property values on the route objects which will effect its behavior. Because options
and defaults
are generically named, it’s hard to know what they do without actually looking at the source.
options
The options
configuration allows you to add option values to your route.
options:
key: value
Options is ust an array of key/value pairs that could be used for anything by anyone. However, if we poke at Symfony’s core code, we’ll see one place where they’re still used
#File: vendor/symfony/routing/Route.php
/**
* Compiles the route.
*
* @return CompiledRoute A CompiledRoute instance
*
* @throws \LogicException If the Route cannot be compiled because the
* path or host pattern is invalid
*
* @see RouteCompiler which is responsible for the compilation process
*/
public function compile()
{
if (null !== $this->compiled) {
return $this->compiled;
}
$class = $this->getOption('compiler_class');
return $this->compiled = $class::compile($this);
}
The compiler_class
option controls how Symfony “compiles” a route. Route compiling is beyond the scope of this article, but the short/incomplete version is the compiler is a class Symfony uses to optimize the route’s matching rules.
If you want to explore more, you can see the default route compiler in the same file.
#File: vendor/symfony/routing/Route.php
public function setOptions(array $options)
{
$this->options = array(
'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
);
return $this->addOptions($options);
}
Take a look at how Symfony\Component\Routing\RouteCompiler
does its work is a great place to start understanding route compiling and Symfony’s internals.
defaults
The last configuration field we’re going to talk about is the confusingly named defaults
key.
path: /foo/{key1}/baz/{key2}/
defaults:
key1: value1
key2: value2
The defaults
key is another collection of key/value pairs. If we’re looking at the PHP Route object, $defaults is a (non-dependency injected) constructor parameter
#File: vendor/symfony/routing/Route.php
public function __construct(
/* ... */
array $defaults = array(),
/* ... */
)
{
/* ... */
$this->addDefaults($defaults);
/* ... */
}
With the usual add/set/get/has methods Symfony often uses for key/value pair data setting.
#File: vendor/symfony/routing/Route.php
public function addDefaults(array $defaults)
{
foreach ($defaults as $name => $default) {
$this->defaults[$name] = $default;
}
$this->compiled = null;
return $this;
}
public function getDefault($name)
{
return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
}
public function hasDefault($name)
{
return array_key_exists($name, $this->defaults);
}
public function setDefault($name, $default)
{
$this->defaults[$name] = $default;
$this->compiled = null;
return $this;
}
Defaults have almost nothing to do with routing. Instead, defaults come into play when you use a route’s unique ID to generate a URL
When you create a route path
in Symfony, you’ll often have place holders
route_name_1:
path: /foo/{id}/
route_name_2:
path: /foo/{place-holder}/
When you’re generating a URL via PHP or twig code, you need to provide values for these place holders
{{ path('route_name_1', {'id':1}) }}
{{ path('route_name_1', {'place-holder':'bar'}) }}
The defaults
key allows you to have a default value for these placeholders. Symfony will use the default values if someone tries to generate a URL without providing a value for these placeholders. If you had a route configuration that looked like this
some_route_name:
path: /foo/{key1}/baz/{key2}/
defaults:
key1: value1
key2: value2
and you generated a URL path with twig code that looks like this
{{ path('some_route_name') }}
you’d end up with a URL path that looked like this
/foo/value1/baz/value2/
The {key1}
and {key2}
placeholder will be automatically replaced with the values from defaults
. There’s no need for the calling code to pass in these values.
Four Special Defaults
In addition to this stock behavior, there are four defaults
keys that Symfony treats as special: _format
, _fragment
, _locale
, and _controller
.
defaults:
# .html|.rss|.etc
_format: ...
# for http://example.com/file.html#fragment URL generation
_fragment: ...
# use URL locales, have it set the response object automatically
_locale: ...
# alternative way to set a controller
_controller: ...
Here’s what these special parameters do.
default: _format
You might use the _format
default like this
path: /file.{_format}
defaults:
_format: .html|.json|.rss
That is, it’s still a standard placeholder. It’s used to give the final section of the URLs a file extension.
More interestingly, it also tells symfony what request headers to use for the request object. Consider the above configuration example and a URL like this
http://symfony.example.com/file.json
Because the file extension (the {_format}
) is json
, Symfony will automatically use a response object that has headers for a JSON response
$ curl -I 'http://symfony.example.com/file.json'
/* ... */
Content-Type: application/json
/* ... */
There would be no need for you to set the headers on the response object (although you would need to make sure your response is a valid JSON object!).
You can (as of Symfony 4.2) find a list of all the supported _formats
(and their Content-Type
‘s in the Request object’s source.
#File: vendor/symfony/http-foundation/Request.php
protected static function initializeFormats()
{
static::$formats = array(
'html' => array('text/html', 'application/xhtml+xml'),
'txt' => array('text/plain'),
'js' => array('application/javascript', 'application/x-javascript', 'text/javascript'),
'css' => array('text/css'),
'json' => array('application/json', 'application/x-json'),
'jsonld' => array('application/ld+json'),
'xml' => array('text/xml', 'application/xml', 'application/x-xml'),
'rdf' => array('application/rdf+xml'),
'atom' => array('application/atom+xml'),
'rss' => array('application/rss+xml'),
'form' => array('application/x-www-form-urlencoded'),
);
}
default: _fragment
The _fragment
default is not a path placeholder, but IS used in URL generation
foo_baz_bar:
path: /the-url
defaults:
_fragment: page2
Symfony will use the _fragment
value to add an #anchor-name
fragment to the end of your URL. For example, with the above route configuration in place, twig code like this
{{ path('foo_baz_bar') }}
{{ path('foo_baz_bar', {'_fragment':'page3'}) }}
would generate URL paths that looked like this
/the-url#page2
/the-url#page3
default: _locale
The _locale
default is another default value for a named path placeholder
route_name:
path: /{_locale}/foo/baz/bar
defaults:
_locale: en
Locale is programmer shorthand for “what country or region is the user coming from, what language should I use in the UI, what culture specific features or language should be enabled/disabled, etc.”. Symfony recommends using locale specific URLs (vs. storing the locale in the session, or some other place).
Like the _format
parameter, _locale
has special powers. If you use {_locale}
in your path
parameter, Symfony will use the URL value to set a locale on Symfony’s Request
object (which, in turn, might trigger other region dependent features).
Another thing to keep in mind: When you use the _locale
default here you’re not setting a default locale for your entire application. You’re setting the default value that Symfony will use when generating a URL, and value Symfony will use if this route is triggered.
If you’re not using a locale in every URL and want to set a default, use the default_locale
setting in your application configuration. Also, Symfony’s stock service configuration uses a locale
(no underscore) parameter that’s used to set the default_locale
, and may be used elsewhere in your application configuration.
default: _controller
Finally, we have one of the stranger defaults, and that’s _controller
.
route_name:
path: /some/path/name
defaults:
_controller: App\Controller\Hello::main
In practical terms, the _controller
default is just a different to set a controller parameter. If you try to set both at the same time
route_name:
path: /some/path/name
controller: App\Controller\Hello::main
defaults:
_controller: App\Controller\Hello::main
Symfony will complain with the following error
Exception thrown when handling an exception (Symfony\Component\Config\Exception\LoaderLoadException: The routing file “/path/to/config/routes.yaml” must not specify both the “controller” key and the defaults key “_controller”
If we go back to the Symfony 2.7 docs, we’ll see that _controller
was the only way to configure a controller if you weren’t using annotations.
I don’t have the background with Symfony to tell you a true history about how this happened, but I have noticed there’s a lot of ambiguity in the Symfony docs when it comes to “routes as annotations” and “routes as configuration”. My suspicions are that the _controller
falls into this ambiguous area. It’s an awkward way to configuration a controller — but might be one of those “here’s an ugly/practical DSL hack to configure a controller even though we’re promoting annotations as the way to do this”.
Regardless — it’s part of Symfony’s history so you’ll want to know how it works.
Wrap Up
That’s it for today. Next time we’re going to put all this service and routing configuration to good use and investigate how Sylius has structured its Symfony based system!