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 fromgetDefault('_controller')
. - By type: The
Route
document returns a value fromgetDefault('type')
, which is then matched against the provided configuration from config.yml - By class: Requires the
Route
document to implementRouteObjectInterface
and return an object forgetContent()
. 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 forgetContent()
. 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
orRouteReferrersInterface
as thename
parameter - Alternatively, supply an implementation of
ContentRepositoryInterface
and the id of the model instance as parametercontent_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:
- Routing for most of the actual functionality implementation
- RoutingBundle for Symfony2 integration bundle for Routing Bundle
- Symfony2's Routing component page
- Handling Multi-Language Documents for some notes on multilingual routing
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.