Symfony2: dynamically add routes

Earlier I was looking for an alternative to Symfony1′s routing.load_configuration event, so I could add some extra routes on-the-fly. This may be useful, when routes change in more ways than only variable request parameters as part of routes do (you know, like /blog/{id}). I got it completely wrong in my previous post about this subject. Of course, adding extra routes is a matter of creating a custom routing loader, and tell the framework about it using the service container. So, there we go.

First we have to create a custom route loader. This class should implement LoaderInterface, which is part of Symfony2′s Config component. It has two methods that are relevant here: supports() and load(). The loader resolver will ask our custom route loader if it can handle a resource (like “@AcmeDemoBundle/Controller/DemoController.php”) of a certain type (like “annotation”). We return true, if the type is “extra”. When this is the case, the resolver will call the load() method. It’s $resource element is irrelevant, since we want to add our extra routes anyway. The load() method should return a RouteCollection containing the new routes we want to add.

namespace Acme\RoutingBundle\Routing;

use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class ExtraLoader implements LoaderInterface
{
    private $loaded = false;

    public function load($resource, $type = null)
    {
        if (true === $this->loaded) {
            throw new \RuntimeException('Do not add this loader twice');
        }

        $routes = new RouteCollection();

        $pattern = '/extra';
        $defaults = array(
            '_controller' => 'AcmeRoutingBundle:Demo:extraRoute',
        );

        $route = new Route($pattern, $defaults);
        $routes->add('extraRoute', $route);

        return $routes;
    }

    public function supports($resource, $type = null)
    {
        return 'extra' === $type;
    }

    public function getResolver()
    {
    }

    public function setResolver(LoaderResolver $resolver)
    {
        // irrelevant to us, since we don't need a resolver
    }
}

Note: make sure the controller you specify really exists.

Now we make a service for our ExtraLoader.

<!-- in /src/Acme/RoutingBundle/Resources/config/services.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="acme.routing_loader" class="Acme\RoutingBundle\Routing\ExtraLoader">
            <tag name="routing.loader"></tag>
        </service>
    </services>
</container>

Notice the tag called “routing.loader”. The delegating routing loader which is used by the framework, will look for all services with the tag “routing.loader” and add them as potential loaders. As said before, when one of these loaders gets a call to supports() and returns true, the load() method will be called and the loader is allowed to add some routes.

The last thing we need, is a few extra lines in /app/config/routing.yml:

AcmeRoutingBundle:
    resource: .
    type: extra

The “resource” key is irrelevant, but required. The important part here is “extra”. This is the “type” which our ExtraLoader supports and thus a call to it’s load() method will definitely be made.

Oh, don’t forget to clear the cache!

Posted in Dependency injection container, Routing, Service, Symfony2 | 12 Comments

12 Responses to Symfony2: dynamically add routes

  • # January 6, 2012 at 14:00

    hi, great solution, but, i can’t get, why there is this line: ‘resource: .’. That dot as a value means null? I thought null is in yaml represented via null or ~.

    rmo

    Reply

    • # January 6, 2012 at 16:02

      Hi, the dot means nothing, but I think using a null value might throw an exception, I will try this later. I just copied the dot notation from the assetic bundle.

      Matthias Noback

      Reply

  • # January 7, 2012 at 11:29

    I guess this is the way in which assetic routes are loaded on the fly when using the controller

    cordoval

    Reply

  • # January 26, 2012 at 00:57

    Great post! Was able to use this information to fix my Doctrine ODM route loader. The “resource: .” was exactly what I needed. Thanks!

    Chris Jones

    Reply

  • Pingback: A week of symfony #262 (2->8 January 2012) « We are php

  • # February 26, 2012 at 05:53

    can you give an example, where the custom route loader could be useful? some some practical usage?

    Mark

    Reply

    • # February 26, 2012 at 10:43

      Hi Mark,
      You can use a custom route loader for adding routes that can be generated automatically, or when you have a bundle and don’t want to ask your users to add the routes to routing.yml manually.

      Matthias Noback

      Reply

  • # February 26, 2012 at 11:39

    Right now I’m trying to implement some kind of dynamic prefix route, but I want it to be optional so that my app will work with or without it:
    localhost/{PREFIX}/app-routes
    localhost/app-routes

    maybe you know what is the best way to do this?
    I tried to add:
    app:
    prefix: /my-prefix
    but this makes the prefix mandatory, and I want it to be optional.

    Thanks.

    Mark

    Reply

    • # February 28, 2012 at 10:16

      Hi, Mark.
      Try to use multiple routes which point to the same controller:
      dattaya_main_test:
      pattern: /{PREFIX}/app-routes
      defaults: { _controller: DattayaMainBundle:Main:test }
      dattaya_main_test1:
      pattern: /app-routes
      defaults: { _controller: DattayaMainBundle:Main:test }
      Or if you’re using annotation:
      * @Route (“/{PREFIX}/app-routes”)
      * @Route (“/app-routes”)

      If you want {PREFIX} to match “/” character also, read http://symfony.com/doc/2.0/cookbook/routing/slash_in_parameter.html

      Dattaya

      Reply

  • # May 4, 2012 at 10:43

    Hi,
    in my case the route rull exist
    /**
    * @Route(“/{extension}/{_locale}”, name=”homepage”, defaults={“_locale” = “fr”, “extension” = “mon-extension”}, requirements={“extension” = “mon-extension”, “_locale” = “en|fr|de|it|es|pl”})
    */

    But all my route params are dynamique
    (i have a service that extract an acount by extension from the databse)
    teh acount have:
    – unique extension
    – default language
    how to custumizethis route with dynamique data (requirement and default shhould be dynamique not only the {extension} ans {_local} params)

    Ahmed

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

* *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>