Skip to content

Dynamic Router

Edit this page

This implementation of a router is configured to load routes from a RouteProviderInterface. This interface can be easily implemented with Doctrine for example. See the following section for more details about the default PHPCR-ODM provider and further below for the Doctrine ORM based implementation. If those do not match your needs, you can build your own route provider.

You can configure the route enhancers that decide what controller is used to handle the request, to avoid hard coding controller names into your route documents.

To fully understand the capabilities of the dynamic router, read also the routing component documentation.

Configuration

The minimum configuration required to load the dynamic router is to specify a route provider backend and to register the dynamic router in the chain of routers.

Note

When your project is also using the CoreBundle, it is enough to configure persistence on cmf_core and you do not need to repeat the configuration for the dynamic router.

1
2
3
4
5
6
7
8
9
10
# app/config/packages/cmf_routing.yaml
cmf_routing:
    chain:
        routers_by_id:
            router.default: 200
            cmf_routing.dynamic_router: 100
    dynamic:
        persistence:
            phpcr:
                enabled: true

When there is no configuration or cmf_routing.dynamic.enabled is set to false, the dynamic router services will not be loaded at all, allowing you to use the ChainRouter with your own routers.

Match Process

Most of the match process is described in the documentation of the CMF Routing component. The only difference is that this bundle will place the contentDocument into the request attributes instead of into the route defaults to avoid issues when generating the URL for the current request.

Your controllers can (and should) declare the parameter $contentDocument in their Action methods if they are supposed to work with content referenced by the routes. Note that the ContentBundle (no longer maintained) provides a default controller that renders the content with a specified template for when you do not need any logic.

A custom controller action can look like this:

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
// src/AppBundle/Controller/ContentController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

/**
 * A custom controller to handle a content specified by a route.
 */
class ContentController extends Controller
{
    /**
     * @param object $contentDocument the name of this parameter is defined
     *      by the RoutingBundle. You can also expect any route parameters
     *      or $template if you configured templates_by_class (see below).
     *
     * @return Response
     */
    public function demoAction($contentDocument)
    {
        // ... do things with $contentDocument and gather other information
        $customValue = 42;

        return $this->render('content/demo.html.twig', [
            'cmfMainContent' => $contentDocument,
            'custom_parameter' => $customValue,
        ]);
    }
}

Note

The DynamicRouter fires an event at the start of the matching process, read more about this in the component documentation.

Configuring the Controller for a Route

To configure what controller is used for which route, you can configure the route enhancers. Many of them operate on routes implementing RouteObjectInterface. This interface tells that the route knows about its content and returns it by the method getRouteContent(). (See CMF Routing component if you want to know more about this interface.)

The possible enhancements that take place, if configured, are (in order of precedence):

  1. (Explicit controller): If there is a _controller set in getRouteDefaults(), no enhancer will overwrite the controller. _template will still be inserted if its not already set;
  2. controllers_by_type: requires the route document to return a 'type' value in getRouteDefaults(). priority: 60;
  3. controllers_by_class: requires the route document to be an instance of RouteObjectInterface and to return an object for getRouteContent(). The content document is checked for being instanceof the class names in the map and if matched that controller is used. Instanceof is used instead of direct comparison to work with proxy classes and other extending classes. priority: 50;
  4. templates_by_class: requires the route document to be an instance of RouteObjectInterface and to return an object for getRouteContent(). The content document is checked for being instanceof the class names in the map and if matched that template will be set as '_template'. priority: 40 for the template, generic controller is set at priority: 30;
  5. If a _template is in the $defaults but no controller was determined so far (neither set on the route nor matched in controller by type or class), the generic controller is chosen. priority: 10;
  6. The default controller is chosen. This controller can use a default template to render the content, which will likely further decide how to handle this content. See also the ContentBundle documentation. priority: -100.

See the configuration reference to learn how to configure these enhancers.

If the ContentBundle is present in your application, the generic and default controllers default to the ContentController provided by that bundle.

Tip

To see some examples, please look at the CMF sandbox and specifically the routing fixtures loading.

Tip

You can also define your own RouteEnhancer classes for specific use cases. See Customizing the Dynamic Router. Use the priority to insert your enhancers in the correct order.

Doctrine PHPCR-ODM Integration

The RoutingBundle comes with a route provider implementation for PHPCR-ODM. PHPCR is well suited to the tree nature of the data. If you use PHPCR-ODM with a route document like the one provided, you can just leave the provider service at the default.

The default provider loads the route at the path in the request and all parent paths to allow for some of the path segments being parameters. If you need a different way to load routes or for example never use parameters, you can write your own provider implementation to optimize by implementing the RouteProviderInterface with your own service and specify that service as cmf_routing.dynamic.route_provider_service_id.

The PHPCR-ODM Route document

All route classes must extend the Symfony core Route class. The default PHPCR-ODM route document also implements the RouteObjectInterface to link routes with content. It maps all features of the core route to the storage, so you can use setDefault, setRequirement, setOption and setHostnamePattern. Additionally when creating a route, you can define whether .{_format} should be appended to the pattern and configure the required _format with a requirements. The other constructor argument lets you control whether the route should append a trailing slash because this can not be expressed with a PHPCR name. The default is to have no trailing slash. Both options can also be changed later through setter methods.

All routes are located under a configured root path, for example /cms/routes. A new route can be created in PHP code as follows:

1
2
3
4
5
6
7
8
use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route;

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

// set explicit controller
$route->setDefault('_controller', 'app.controller::specialAction');

The above example should probably be done as a route configured in a Symfony configuration file, unless the end user is supposed to change the URL or the controller.

To link a content to this route, simply set it on the document:

1
2
3
4
5
use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\Content;

// ...
$content = new Content('my content'); // Content must be a mapped class
$route->setRouteContent($content);

This will make the routing put the document into the request parameters and if your controller specifies a parameter called $contentDocument, it will be passed this document.

You can also use variable patterns for the URL and define requirements with setRequirement and defaults with setDefault:

1
2
3
4
// do not forget leading slash if you want /projects/{id} and not /projects{id}
$route->setVariablePattern('/{id}');
$route->setRequirement('id', '\d+');
$route->setDefault('id', 1);

This defines a route that matches the URL /projects/<number> but also /projects as there is a default for the id parameter. This will match /projects/7 as well as /projects but not /projects/x-4. The document is still stored at /routes/projects. This will work because, as mentioned above, the route provider will look for route documents at all possible paths and pick the first that matches. In our example, if there is a route document at /routes/projects/7 that matches (no further parameters), it gets chosen. Otherwise, routing checks if /routes/projects has a pattern that matches. If not, the top document at /routes is checked for a matching pattern.

The semantics and rules for patterns, defaults and requirements are exactly the same as in core routes. If you have several parameters, or static bits after a parameter, make them part of the variable pattern:

1
2
3
$route->setVariablePattern('/{context}/item/{id}');
$route->setRequirement('context', '[a-z]+');
$route->setRequirement('id', '\d+');

Note

The RouteDefaultsValidator validates the route defaults parameters. For more information, see Customizing the Dynamic Router.

With the above example, your controller can expect both the $id parameter as well as the $contentDocument if you set a content on the route and have a variable pattern with {id}. The content could be used to define an intro section that is the same for each id. If you don't need content, you can also omit setting a content document on the route document.

Note

See below for information about routes and translation.

Doctrine ORM integration

Alternatively, you can use the Doctrine ORM provider by specifying the persistence.orm part of the configuration. It does a similar job but, as the name indicates, loads Route entities from an ORM database.

Caution

You must install the CoreBundle to use this feature if your application does not have at least DoctrineBundle 1.3.0.

The ORM Route entity

The example in this section applies if you use the ORM route provider (Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Orm\RouteProvider). It uses the staticPrefix field of the Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Orm\Route to find route candidates.

Symfony Cmf routing system allows us loading whatever content from a route. That means an entity route can reference to different types of entities. But Doctrine ORM is not able to establish that kind of mapping associations. To do that, the ORM RouteProvider follows the pattern of FQN:id. That is, the full model class name, then a colon, then the id. You only need to add it to the defaults parameters of the route with the RouteObjectInterface::CONTENT_ID key. cmf_routing.content_repository service can help you to do it easily. 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
// src/AppBundle/DataFixtures/ORM/LoadPostData.php
namespace AppBundle\DataFixtures\ORM;

use AppBundle\Entity\Post;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Orm\Route;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class LoadPostData implements FixtureInterface, ContainerAwareInterface
{
    use ContainerAwareTrait;

    /**
     * @param ObjectManager $manager
     */
    public function load(ObjectManager $manager)
    {
        $post = new Post();
        $post->setTitle('My Content');
        $manager->persist($post);
        $manager->flush(); // flush to be able to use the generated id

        $contentRepository = $this->container->get('cmf_routing.content_repository');

        $route = new Route();
        $route->setName('my-content');
        $route->setStaticPrefix('/my-content');
        $route->setDefault(RouteObjectInterface::CONTENT_ID, $contentRepository->getContentId($post));
        $route->setContent($post);
        $post->addRoute($route); // Create the backlink from content to route

        $manager->persist($post);
        $manager->flush();
    }
}

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

Caution

Make sure that the content already has an id before you set it on the route. The route to content link only works with single column ids.

The Post entity content in this example could be like this:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// src/AppBundle/Entity/Post.php
namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Cmf\Component\Routing\RouteReferrersInterface;

/**
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
 */
class Post implements RouteReferrersInterface
{
    /** .. fields like title and body */

    /**
     * @var RouteObjectInterface[]|ArrayCollection
     *
     * @ORM\ManyToMany(targetEntity="Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Orm\Route", cascade={"persist", "remove"})
     */
    private $routes;

    public function __construct()
    {
        $this->routes = new ArrayCollection();
    }

    /**
     * @return RouteObjectInterface[]|ArrayCollection
     */
    public function getRoutes()
    {
        return $this->routes;
    }

    /**
     * @param RouteObjectInterface[]|ArrayCollection $routes
     */
    public function setRoutes($routes)
    {
        $this->routes = $routes;
    }

    /**
     * @param RouteObjectInterface $route
     *
     * @return $this
     */
    public function addRoute($route)
    {
        $this->routes[] = $route;

        return $this;
    }

    /**
     * @param RouteObjectInterface $route
     *
     * @return $this
     */
    public function removeRoute($route)
    {
        $this->routes->removeElement($route);

        return $this;
    }
}

Because you set the content_id default value on the route, the controller can expect the $contentDocument parameter. You can now configure which template or which special controller should handle Post entities with the templates_by_type resp. controllers_by_type configuration as explained in Configuration Reference.

The ORM routes support more things, for example route parameters, requirements and defaults. This is explained in the route document section.

Locales and Route Objects

Neither PHPCR-ODM nor ORM routes should be translated documents themselves. A route represents one single url, and serving several translations under the same url is not recommended.

Make sure you configure the valid locales in the configuration so that the bundle can optimally handle locales. The configuration reference lists some options to tweak behavior and performance.

For multilingual websites, you have two options: One route object per language with the _locale set as default; A single route with the option add_locale_pattern.

One Route per Language

With this approach, you store a separate route object for each language. Each of those routes points to the same translated content object. To get the language picked up into Symfony, set a default value _locale to the corresponding local.

This will then be picked up by the Symfony translation system.

The ContentAwareGenerator respects the _locale default when choosing which route to generate for a content.

This approach has the upside that you can have completely translated URLs, which can be desired for better readability of the URLs and SEO purposes.

Single Route with Locale Pattern

If you are happy with the same URL for each language and only want a different locale prefix, you can store a single route object created with the option add_locale_pattern set to true.

This flag makes the route provide prepend /{_locale} to its path, meaning it will match the first part of the path as locale. The locale will then be picked up by Symfony.

URL generation with the DynamicRouter

Apart from matching an incoming request to a set of parameters, a Router is also responsible for generating an URL from a route and its parameters. The DynamicRouter adds more power to the URL generating capabilities of Symfony.

Tip

All Twig examples below are given with the path function that generates the URL without domain, but will work with the url function as well.

Also, you can specify other parameters to the generator, which will be used if the route contains a dynamic pattern or otherwise will be appended as query string, just like with the standard routing.

2.3

Since `symfony-cmf/routing: 2.3.0`, the route document should be passed in the route parameters as `_route_object`, and the special route name `cmf_routing_object` is to be used. When using older versions of routing, you need to pass the route document as route name.

You can use a Route object directly with the router:

1
2
{# myRoute is an object of class Symfony\Component\Routing\Route #}
<a href="{{ path('cmf_routing_object', {_route_object: myRoute}) }}">Read on</a>

When using the PHPCR-ODM persistence layer, the repository path of the route document is considered the route name. Thus you can specify a repository path to generate a route:

1
2
{# Create a link to / on this server #}
<a href="{{ path('/cms/routes') }}>Home</a>

Caution

It is dangerous to hard-code paths to PHPCR-ODM documents into your templates. An admin user could edit or delete them in a way that your application breaks. If the route must exist for sure, it probably should be a statically configured route. But route names could come from code for example.

The DynamicRouter uses a URL generator that operates on the RouteReferrersInterface. This means you can also generate a route from any object that implements this interface and provides a route for it:

1
2
{# myContent implements RouteReferrersInterface #}
<a href="{{ path('cmf_routing_object', {_route_object: myContent}) }}>Read on</a>

Tip

If there are several routes for the same content, the one with the locale matching the current request locale is preferred

Additionally, the generator also understands the content_id parameter with an empty route name and tries to find a content implementing the RouteReferrersInterface from the configured content repository:

1
2
3
<a href="{{ path('cmf_routing_object', {'content_id': '/cms/content/my-content'}) }}>
    Read on
</a>

Note

To be precise, it is enough for the content to implement the RouteReferrersReadInterface if writing the routes is not desired. See Bundle Standards for more on the naming scheme.

For the implementation details, please refer to the Dynamic Router section in the the cmf routing component documentation.

The RouterInterface defines the method getRouteCollection to get all routes available in a router. The DynamicRouter is able to provide such a collection, however this feature is disabled by default to avoid dumping large numbers of routes. You can set cmf_routing.dynamic.route_collection_limit to a value bigger than 0 to have the router return routes up to the limit or false to disable limits and return all routes.

With this option activated, tools like the router:debug command or the FOSJsRoutingBundle will also show the routes coming from the database.

For the case of FOSJsRoutingBundle, if you use the upcoming version 2 of the bundle, you can configure fos_js_routing.router to router.default to avoid the dynamic routes being included.

Handling RedirectRoutes

This bundle also provides a controller to handle RedirectionRouteInterface documents. You need to configure the route enhancer for this interface:

1
2
3
4
5
# app/config/packages/cmf_routing.yaml
cmf_routing:
    dynamic:
        controllers_by_class:
            Symfony\Cmf\Component\Routing\RedirectRouteInterface: cmf_routing.redirect_controller::redirectAction
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version