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
_controller
set ingetRouteDefaults()
, no enhancer will overwrite the controller._template
will 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 ofRouteObjectInterface
and to return an object forgetRouteContent()
. The content document is checked for beinginstanceof
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;templates_by_class
: requires the route document to be an instance ofRouteObjectInterface
and to return an object forgetRouteContent()
. The content document is checked for beinginstanceof
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;- 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; - 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.