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 | 49 Comments

49 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

      • # February 28, 2013 at 08:16

        hi Matthias Noback ,, i m new to symfony2 with php i just want to know how to make a index.php inside web dir ,which contain the routing of spefic bundle routing from inside itself
        .. can u give me ur gmail id or any other where i can contact to u and can do live chat with u ?

        ramkesh

        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

  • # June 22, 2012 at 08:48

    Great article.

    My only problem is that I need to clear the cache with every change even when using the production environment. Any suggestions?

    Daniel

    Reply

    • # June 26, 2012 at 13:07

      I’m sure it must be possible to clear the cache from within a running application (after the change was made), though I wouldn’t know from the top of my head how to do that.

      Matthias Noback

      Reply

    • # July 13, 2012 at 15:32

      You can use a “/{anything}” route, with requirement “anything” = “.+” and than programmatically parse the URL. You can then create your own cache for the way you determine the result.

      Matthias Noback

      Reply

  • # September 3, 2012 at 14:43

    Thanks for sharing your findings. I could successfully implement this for my project. However I have one open question: To build the rules I need to access the doctrine entity manager but since I don’t get the kernel or the container I have nothing to I could ask it.

    Any ideas how to access the doctrine entity manager from withing the load() method?

    Ernst

    Reply

    • # September 3, 2012 at 15:48

      Hi Ernst,

      This is possible with dependency injection, add this to the loader:

      use Doctrine\ORM\EntityManager
      
      class ExtraLoader implements LoaderInterface
      {
          private $em;
      
          public function __construct(EntityManager $em)
          {
              $this->em = $em;
          }
      }
      

      Then expand the service definition:

      <service id="acme.routing_loader" class="Acme\RoutingBundle\Routing\ExtraLoader">
          <argument type="service" id="doctrine.orm.entity_manager" />
          <tag name="routing.loader"></tag>
      </service>
      

      Good luck!

      Matthias

      Matthias Noback

      Reply

      • # September 3, 2012 at 17:55

        Thanks a lot that looks promising (and I think I just learned a lot about dependency injection). However I’m failing to translate this into YAML. Is there someone who can show me how this XML has to look like in YAML?

        Ernst

        Reply

        • # September 4, 2012 at 08:11

          After all I found it myself:


          services:
          people.routing_loader:
          class: path\to\bundle\MyBundle\Routing\EntityRouterLoader
          argument: [doctrine.orm.entity_manager]
          tags:
          - { name: routing.loader }

          Ernst

          Reply

      • # March 10, 2013 at 17:13

        Catchable Fatal Error: Argument 1 passed to Acme\HelloBundle\Routing\ExtraLoader::__construct() must be an instance of Doctrine\ORM\EntityManager, none given, called in /var/www/domains/sf2.loc/Symfony/app/cache/dev/appDevDebugProjectContainer.php on line 1437 and defined in /var/www/domains/sf2.loc/Symfony/src/Acme/HelloBundle/Routing/ExtraLoader.php line 27

        ivan

        Reply

        • # March 11, 2013 at 20:30

          I don’t know Ivan, your service definition does not seem to be right – maybe it misses the doctrine.orm.entity_manager argument.

          Matthias Noback

          Reply

          • # March 12, 2013 at 07:35

            I can send you the code if you do not mind. Total 4 files. Please help me solve this problem.

            ivan

        • # March 22, 2013 at 20:17

          Could you create a gist (https://gist.github.com/) or something?

          Matthias Noback

          Reply

  • # September 17, 2012 at 07:37

    Thanks for example!

    Can you tell me will it cached or not?

    John

    Reply

    • # September 17, 2012 at 07:44

      Hi John, it will certainly be cached. For production, the routing is loaded only once and then dumped to the cache folder (refer to the generated UrlGenerator and UrlMatcher classes in /app/cache to see if the loading process was successful).

      Matthias Noback

      Reply

      • # September 17, 2012 at 08:57

        Thanks!

        John

        Reply

  • # September 27, 2012 at 11:04

    Dear Matthias,

    I am trying to use your tutorial and I am getting the error: Cannot load resource “.”.

    Do you have an idea on how to resolve this.

    Thank you very much.

    Milos

    Reply

    • # September 29, 2012 at 19:58

      I have not tried to run this example recently – my guess would be that somehow the loader service is not well defined (missing tag or something) or the services.xml file is not loaded correctly. One other option: I have tested this with Symfony 2.0, it might not work in 2.1 anymore. Please let me know if you have found the solution.

      Matthias Noback

      Reply

  • # October 11, 2012 at 21:09

    Hello, great implementation. I am trying to add dynamic routers using your idea and I get an error:

    Circular reference detected in “/../app/config/routing.yml” (“/../app/config/routing.yml” > “router_loader_extras” > “/../app/config/routing.yml”).

    Please, any suggestions?

    Max Martínez

    Reply

    • # October 14, 2012 at 10:58

      It is possible that you have indeed made a circular reference somehow, which means that you load routing.yml, while you are already loading it. But most of the times, this exception means that your routing contains syntax errors, for example a missing or unavailable annotation. You should check out the previous exceptions, deeper in the hierarchy.

      Matthias Noback

      Reply

  • # October 25, 2012 at 02:04

    Hello Matthias Noback,

    Thanks a lot for posting this tutorial.

    I was confused how to simplify common CRUD routing.
    But now I have workaround using resource as you can see in this class.
    https://github.com/epsi/AlumniBook-SF2/blob/1004aea21a44e6ed8b664570e6d6cf4816fcd586/src/Iluni/BookBundle/Library/Routing/CrudLoader.php

    Before and after using CrudLoader can be seen here.
    https://github.com/epsi/AlumniBook-SF2/blob/1004aea21a44e6ed8b664570e6d6cf4816fcd586/src/Iluni/BookBundle/Resources/config/routing/crud/acompetencies.yml

    I don’t know if my approach is right or bad practice.
    It works well so far.

    Once again. Thank you.

    ~epsi — sorry for my english

    epsi

    Reply

    • # October 28, 2012 at 10:24

      Thanks for sharing, this looks very nice!

      Matthias Noback

      Reply

  • # October 25, 2012 at 15:13

    Under Symfony 2.1.2 i got this error:

    Pixo\Modules\NewsBundle\Routing\ExtraLoader::setResolver() must be compatible with Symfony\Component\Config\Loader\LoaderInterface::setResolver(Symfony\Component\Config\Loader\LoaderResolverInterface $resolver) in /usr/share/nginx/www/Symfonya/sfvalet/src/Pixo/Modules/NewsBundle/Routing/ExtraLoader.php

    How can i fix this ?

    Shijima

    Reply

    • # October 25, 2012 at 15:40

      Solved ! thank’s.

      Shijima

      Reply

  • # October 26, 2012 at 08:19

    I’m trying to use the service in a controller passing new route to the load function with no luck (router:debug shows only routes added in ExtraLoader) … what’s wrong ?

    Shijima

    Reply

    • # October 28, 2012 at 10:27

      Hi Shjima,

      I’m not sure if I understand tour question fully. But adding routes from within a controller is too late. It should be done when the routing is loaded, which means you should (like described above) add resources to the main routing.yml. Good luck!

      Matthias Noback

      Reply

  • # November 24, 2012 at 13:54

    Dear Matthias,

    Thank you for this wonderful tut. I have 1 question which I hope you can help me with:
    I’m in the situation where my bundle is loaded after the framework bundle is (because my bundle is relying on certain parts of the framework bundle). The problem is that in this case it seems like adding the extraloader via my bundle config doesnt seem to have any effect because the RoutingResolverPass has already been run before in the framework bundle. Any word of advice for me?

    Raine

    Reply

    • # November 26, 2012 at 09:26

      What is your specific dependency on the framework bundle? Maybe there is a workaround for that part.

      Matthias Noback

      Reply

  • # January 19, 2013 at 21:41

    Works great under Symfony 2.2.0 BETA1.

    Just change LoadResolver to LoadResolverInterface.

    Art Hundiak

    Reply

  • # March 13, 2013 at 20:39

    Nice article,
    I am interested though once I add those dynamically generated routes, how do i refer to them in twig or the router service like the path() and generateUrl methods? Thanks!

    Feras

    Reply

  • # March 24, 2013 at 11:13

    Matthias please add word-break: break-all; to the divs so to wrap long links :)

    Luis Cordova

    Reply

  • # March 24, 2013 at 16:14

    This is great, however how do you get around the caching issue? I’ve created my own CMS where pages can be added, however the dynamic routing works for the first page you land on e.g if you go to /about-us however no other pages are accessible because the routing file get’s cached so bypasses this script.

    David

    Reply

    • # March 25, 2013 at 11:11

      Hi David,
      You can not truly define your routes dynamically using this strategy. Your route loader will not be called for subsequent requests. If you can determine certain routes only at runtime, you could use a route pattern like /{dynamic} with this requirement for “dynamic”: .+. You can then forward to the right controller from within the controller this route points to.

      Matthias Noback

      Reply

  • # March 26, 2013 at 22:09

    When i return a RouteCollercion under my routeing news.yml

    esolving_pageB_news:
    resource: ‘@EsolvingPageBundle/Resources/config/routing/news_real.yml’
    # resource: ‘/var/www/EsolvingSevenpharma/src/Esolving/PageBundle/Resources/config/routing/news_real.yml’
    prefix: /

    Cannot import resource “@EsolvingPageBundle/Resources/config/routing/news_real.yml” from “routing/news.yml”. Make sure the “EsolvingPageBundle” bundle is correctly registered and loaded in the application kernel class.

    but in my kernel is registered EsolvingPageBundle… help me please.

    luis

    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>