Dynamic Router
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):
- (Explicit controller): If there is a
_controllerset ingetRouteDefaults(), no enhancer will overwrite the controller._templatewill still be inserted if its not already set; controllers_by_type: requires the route document to return a 'type' value ingetRouteDefaults(). priority: 60;controllers_by_class: requires the route document to be an instance ofRouteObjectInterfaceand to return an object forgetRouteContent(). The content document is checked for beinginstanceofthe class names in the map and if matched that controller is used.Instanceofis used instead of direct comparison to work with proxy classes and other extending classes. priority: 50;templates_by_class: requires the route document to be an instance ofRouteObjectInterfaceand to return an object forgetRouteContent(). The content document is checked for beinginstanceofthe 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;- If a
_templateis in the$defaultsbut 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; - 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). It uses the
staticPrefix field of the
Symfony 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.
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
Customize the DynamicRouter
Read on in the chapter customizing the dynamic router.