Categories


Archives


Recent Posts


Categories


Symfony Routing Configuration Keys

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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

The controller configuration

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

The host configuration

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

The schemes configuration

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

The methods configuration

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? Every path in that file needs to start with the value of this prefix 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.ymlis 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!

Series Navigation<< Symfony Routes and Stand Alone ControllersBasic and Advanced Sylius Routing >>

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 1st April 2019