Routing

Routing

This is an introduction to understand the concepts behind CMF routing. For the reference documentation please see the documentation for the Routing component and the RoutingBundle.

Concept

Why a new Routing Mechanism?

CMS are highly dynamic sites, where most of the content is managed by the administrators rather than developers. The number of available pages can easily reach the thousands, which is usually multiplied by the number of available translations. Best accessibility and SEO practices, as well as user preferences dictate that the URLs should be definable by the content managers.

The default Symfony2 routing mechanism, with its configuration file approach, is not the best solution for this problem. It does not provide a way of handling dynamic, user-defined routes, nor does it scale well to a large number of routes.

The Solution

In order to address these issues, a new routing system needed to be developed that takes into account the typical needs of CMS routing:

  • User-defined URLs;
  • Multi-site;
  • Multi-language;
  • Tree-like structure for easier management;
  • Content, Menu and Route separation for added flexibility.

The Symfony CMF Routing component was created with these requirements in mind.

The ChainRouter

At the core of Symfony CMF's Routing component sits the ChainRouter. It is used as a replacement for Symfony2's default routing system and, like the Symfony2 router, is responsible for determining which Controller will handle each request.

The ChainRouter works by accepting a set of prioritized routing strategies, RouterInterface implementations, commonly referred to as "Routers". The routers are responsible for matching an incoming request to an actual Controller and, to do so, the ChainRouter iterates over the configured Routers according to their configured priority:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    # app/config/config.yml
    cmf_routing:
        chain:
            routers_by_id:
                # enable the DynamicRouter with a low priority
                # this way the non dynamic routes take precedence
                # to prevent needless database look ups
                cmf_routing.dynamic_router: 20
    
                # enable the symfony default router with a higher priority
                router.default: 100
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <container xmlns="http://cmf.symfony.com/schema/dic/services">
    
        <config xmlns="http://cmf.symfony.com/schema/dic/routing">
            <chain>
                <!-- enable the DynamicRouter with a low priority
                     this way the non dynamic routes take precedence
                     to prevent needless database look ups -->
                <routers-by-id
                    id="cmf_routing.dynamic_router">
                    20
                </routers-by-id>
    
                <!-- enable the symfony default router with a higher priority -->
                <routers-by-id
                    id="router.default">
                    100
                </routers-by-id>
            </chain>
        </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // app/config/config.php
    $container->loadFromExtension('cmf_routing', array(
        'chain' => array(
            'routers_by_id' => array(
                // enable the DynamicRouter with a low priority
                // this way the non dynamic routes take precedence
                // to prevent needless database look ups
                'cmf_routing.dynamic_router' => 20,
    
                // enable the symfony default router with a higher priority
                'router.default' => 100,
            ),
        ),
    ));
    

You can also load Routers using tagged services, by using the router tag and an optional priority. The higher the priority, the earlier your router will be asked to match the route. If you do not specify the priority, your router will come last. If there are several routers with the same priority, the order between them is undetermined. The tagged service will look like this:

  • YAML
    1
    2
    3
    4
    5
    services:
        my_namespace.my_router:
            class: "%my_namespace.my_router_class%"
            tags:
                - { name: router, priority: 300 }
    
  • XML
    1
    2
    3
    <service id="my_namespace.my_router" class="%my_namespace.my_router_class%">
        <tag name="router" priority="300" />
    </service>
    
  • PHP
    1
    2
    3
    4
    $container
        ->register('my_namespace.my_router', '%my_namespace.my_router_class%')
        ->addTag('router', array('priority' => 300))
    ;
    

The Symfony CMF Routing system adds a new DynamicRouter, which complements the default Router found in Symfony2.

The Default Symfony2 Router

Although it replaces the default routing mechanism, Symfony CMF Routing allows you to keep using the existing system. In fact, the standard Symfony2 routing is enabled by default, so you can keep using the routes you declared in your configuration files, or as declared by other bundles.

The DynamicRouter

This Router can dynamically load Route instances from a dynamic source via a so called provider. In fact it only loads candidate routes. The actual matching process is exactly the same as with the standard Symfony2 routing mechanism. However the DynamicRouter additionally is able to determine which Controller and Template to use based on the Route that is matched.

By default the DynamicRouter is disabled. To activate it, just add the following to your configuration file:

  • YAML
    1
    2
    3
    4
    # app/config/config.yml
    cmf_routing:
        dynamic:
            enabled: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <container xmlns="http://cmf.symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
        <config xmlns="http://cmf.symfony.com/schema/dic/routing">
            <dynamic enabled="true" />
        </config>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/config.php
    $container->loadFromExtension('cmf_routing', array(
        'dynamic' => array(
            'enabled' => true,
        ),
    ));
    

This is the minimum configuration required to load the DynamicRouter as a service, thus making it capable of performing routing. Actually, when you browse the default pages that come with the Symfony CMF SE, it is the DynamicRouter that matches your requests with the Controllers and Templates.

Getting the Route Object

The provider to use can be configured to best suit each implementation's needs. As part of this bundle, an implementation for Doctrine ORM and PHPCR-ODM is provided. Also, you can easily create your own by simply implementing the RouteProviderInterface. Providers are responsible for fetching an ordered subset of candidate routes that could match the request. For example the default PHPCR-ODM provider loads the Route at the path in the request and all parent paths to allow for some of the path segments being parameters.

For more detailed information on this implementation and how you can customize or extend it, refer to RoutingBundle.

The DynamicRouter is able to match the incoming request to a Route object from the underlying provider. The details on how this matching process is carried out can be found in the component documentation.

Note

To have the route provider find routes, you also need to provide the data in your storage. With PHPCR-ODM, this is either done through the admin interface (see at the bottom) or with fixtures.

However, before we can explain how to do that, you need to understand how the DynamicRouter works. An example will come later in this document.

Getting the Controller and Template

A Route needs to specify which Controller should handle a specific Request. The DynamicRouter uses one of several possible methods to determine it (in order of precedence):

  • Explicit: The Route document itself can explicitly declare the target Controller if one is returned from getDefault('_controller').
  • By type: The Route document returns a value from getDefault('type'), which is then matched against the provided configuration from config.yml
  • By class: Requires the Route document to implement RouteObjectInterface and return an object for getContent(). The returned class type is then matched against the provided configuration from config.yml.
  • Default: If configured, a default Controller will be used.

Apart from this, the DynamicRouter is also capable of dynamically specifying which Template will be used, in a similar way to the one used to determine the Controller (in order of precedence):

  • Explicit: The stored Route document itself can explicitly declare the target Template by returning the name of the template via getDefault('_template').
  • By class: Requires the Route instance to implement RouteObjectInterface and return an object for getContent(). The returned class type is then matched against the provided configuration from config.yml.

Here's an example of how to configure the above mentioned options:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # app/config/config.yml
    cmf_routing:
        dynamic:
            generic_controller: cmf_content.controller:indexAction
            controllers_by_type:
                editable_static: sandbox_main.controller:indexAction
            controllers_by_class:
                Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: cmf_content.controller::indexAction
            templates_by_class:
                Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: CmfContentBundle:StaticContent:index.html.twig
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <container xmlns="http://cmf.symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
        <config xmlns="http://cmf.symfony.com/schema/dic/routing">
            <dynamic generic-controller="cmf_content.controller:indexAction">
                <controllers-by-type type="editable_static">
                    sandbox_main.controller:indexAction
                </controllers-by-type>
    
                <controllers-by-class
                    class="Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent"
                >
                    cmf_content.controller::indexAction
                </controllers-by-class>
    
                <templates-by-class class="Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent"
                >
                    CmfContentBundle:StaticContent:index.html.twig
                </templates-by-class>
            </dynamic>
        </config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // app/config/config.php
    $container->loadFromExtension('cmf_routing', array(
        'dynamic' => array(
            'generic_controller' => 'cmf_content.controller:indexAction',
            'controllers_by_type' => array(
                'editable_static' => 'sandbox_main.controller:indexAction',
            ),
            'controllers_by_class' => array(
                'Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent' => 'cmf_content.controller::indexAction',
            ),
            'templates_by_class' => array(
                'Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent' => 'CmfContentBundle:StaticContent:index.html.twig',
            ),
        ),
    ));
    

Notice that enabled: true is no longer present. It's only required if no other configuration parameter is provided. The router is automatically enabled as soon as you add any other configuration to the dynamic entry.

Note

This example uses a controller which is defined as a service. You can also configure a controller by using a fully qualified class name: CmfContentBundle:Content:index.

For more information on using controllers as a service read cook book section How to Define Controllers as Services

Note

Internally, the routing component maps these configuration options to several RouteEnhancerInterface instances. The actual scope of these enhancers is much wider, and you can find more information about them in the routing enhancers documentation section.

Linking a Route with a Model Instance

Depending on your application's logic, a requested URL may have an associated model instance from the database. Those Routes can implement the RouteObjectInterface, and optionally return a model instance, that will be automatically passed to the Controller as the contentDocument method parameter.

Note that a Route can implement the above mentioned interface but still not return any model instance, in which case no associated object will be provided.

Furthermore, Routes that implement this interface can also provide their own name with the getRouteKey method. For normal Symfony routes, the name is only known from their key in the RouteCollection collection hashmap. In the CMF, it is possible to use route documents outside of collections, and thus useful to have routes provide their name. The PHPCR routes for example return the repository path when this method is called.

Redirects

You can build redirects by implementing the RedirectRouteInterface. If you are using the default PHPCR-ODM route provider, a ready to use implementation is provided in the RedirectRoute Document. It can redirect either to an absolute URI, to a named Route that can be generated by any Router in the chain or to another Route object known to the route provider. The actual redirection is handled by a specific Controller that can be configured as follows:

  • YAML
    1
    2
    3
    4
    5
    # app/config/config.yml
    cmf_routing:
        dynamic:
            controllers_by_class:
                Symfony\Cmf\Component\Routing\RedirectRouteInterface: cmf_routing.redirect_controller:redirectAction
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <container xmlns="http://cmf.symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
        <config xmlns="http://cmf.symfony.com/schema/dic/routing">
            <dynamic>
                <controllers-by-class
                    class="Symfony\Cmf\Component\Routing\RedirectRouteInterface">
                    cmf_routing.redirect_controller:redirectAction
                </controllers-by-class>
            </dynamic>
        </config>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/config.php
    $container->loadFromExtension('cmf_routing', array(
        'dynamic' => array(
            'controllers_by_class' => array(
                'Symfony\Cmf\Component\Routing\RedirectRouteInterface' => 'cmf_routing.redirect_controller:redirectAction',
            ),
        ),
    ));
    

Note

The actual configuration for this association exists as a service, not as part of a config.yml file. As discussed before, any of the approaches can be used.

URL Generation

Symfony CMF's Routing component uses the default Symfony2 components to handle route generation, so you can use the default methods for generating your URLs with a few added possibilities:

  • Pass an implementation of either RouteObjectInterface or RouteReferrersInterface as the name parameter
  • Alternatively, supply an implementation of ContentRepositoryInterface and the id of the model instance as parameter content_id

See URL generation with the DynamicRouter for code examples of all above cases.

The route generation handles locales as well, see "ContentAwareGenerator and Locales".

The PHPCR-ODM Route Document

As mentioned above, you can use any route provider. The example in this section applies if you use the default PHPCR-ODM route provider (Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\RouteProvider).

PHPCR-ODM documents are stored in a tree, and their ID is the path in that tree. To match routes, a part of the repository path is used as URL. To avoid mixing routes and other documents, routes are placed under a common root path and that path is removed from the ID to build the URL. The common root path is called "route basepath". The default base path is /cms/routes. A new route can be created in PHP code as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// src/Acme/MainBundle/DataFixtures/PHPCR/LoadRoutingData.php
namespace Acme\DemoBundle\DataFixtures\PHPCR;

use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route;
use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\StaticContent;
use PHPCR\Util\NodeHelper;

class LoadRoutingData implements FixtureInterface
{
    /**
     * @param DocumentManager $dm
     */
    public function load(ObjectManager $dm)
    {
        if (!$dm instanceof DocumentManager) {
            $class = get_class($dm);
            throw new \RuntimeException("Fixture requires a PHPCR ODM DocumentManager instance, instance of '$class' given.");
        }

        $session = $dm->getPhpcrSession();
        NodeHelper::createPath($session, '/cms/routes');

        $route = new Route();
        $route->setParentDocument($dm->find(null, '/cms/routes'));
        $route->setName('my-page');

        // link a content to the route
        $content = new StaticContent();
        $content->setParentDocument($dm->find(null, '/cms/content'));
        $content->setName('my-content');
        $content->setTitle('My Content');
        $content->setBody('Some Content');
        $dm->persist($content);
        $route->setContent($content);

        $dm->persist($route);
        $dm->flush();
    }
}

Now the CMF will be able to handle requests for the URL /my-content.

Caution

As you can see, the code explicitly creates the /cms/routes path. The RoutingBundle only creates this path automatically if the Sonata Admin was enabled in the routing configuration using an initializer. Otherwise, it'll assume you do something yourself to create the path (by configuring an initializer or doing it in a fixture like this).

Because you called setContent on the route, the controller can expect the $contentDocument parameter. You can now configure which controller should handle StaticContent as explained above.

The PHPCR-ODM routes support more things, for example route parameters, requirements and defaults. This is explained in the route document section in the RoutingBundle documentation.

Further Notes

For more information on the Routing component of Symfony CMF, please refer to:

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.