Symfony2: dynamically add routes

Posted on by Matthias Noback

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!

PHP Symfony2 dependency injection routing

I have removed Disqus from this website, so for now, you can't comment on articles. In the near future you will be able to send comments by email. For now, if you want to say something, you can always send an email to me personally: info@matthiasnoback.nl.

Comments
rmo

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 ~.

Matthias Noback

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.

ramkesh

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 ?

Matthias Noback

I'm sorry - I won't have the time to help you with this. Please refer to the documentation on symfony.com (especially http://symfony.com/doc/curr....

cordoval

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

Matthias Noback

That's right, as I mentioned in another comment above.

Matthias Noback

Another interesting approach is the one by Kris Wallsmith, the author of Assetic: he adds a few lines to the cached "routing.yml" file, which tell the router to also load the resource of type "assetic". See https://github.com/symfony/...

Chris Jones

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

Mark

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

Matthias Noback

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.

Mark

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.

Dattaya

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/...

Ahmed

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)

Daniel

Great article.

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

Matthias Noback

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

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.

Ernst

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?

Matthias Noback

Hi Ernst,

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

[php]
use Doctrine\ORM\EntityManager

class ExtraLoader implements LoaderInterface
{
private $em;

public function __construct(EntityManager $em)
{
$this->em = $em;
}
}
[/php]

Then expand the service definition:

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

Good luck!

Matthias

Ernst

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

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 }

ivan

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

Matthias Noback

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

ivan

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

Matthias Noback

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

John

Thanks for example!

Can you tell me will it cached or not?

Matthias Noback

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).

John

Thanks!

Milos

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.

Matthias Noback

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.

Max Martínez

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?

Matthias Noback

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.

epsi

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/Alu...

Before and after using CrudLoader can be seen here.
https://github.com/epsi/Alu...

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

Matthias Noback

Thanks for sharing, this looks very nice!

Shijima

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

Solved ! thank's.

Bart

What is the solution for this problem ?

PHP Fatal error: Declaration of MR\RegiomotoBundle\Routing\ExtraLoader::setResolver() must be compatible with that of Symfony\Component\Config\Loader\LoaderInterface::setResolver() in /home/bart/www/mobile.kody-etap8/src/MR/RegiomotoBundle/Routing/ExtraLoader.php on line 18

Matthias Noback

The method signatures don't match. For Symfony 2.0:

[php]
public function setResolver(LoaderResolver $resolver);
[/php]

For newer versions:

[php]
public function setResolver(LoaderResolverInterface $resolver);
[/php]

Shijima

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 ?

Matthias Noback

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!

Raine

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?

Matthias Noback

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

Art Hundiak

Works great under Symfony 2.2.0 BETA1.

Just change LoadResolver to LoadResolverInterface.

Matthias Noback

Thanks for your comment, I will revise this later because of https://github.com/symfony/...

Feras

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!

Luis Cordova

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

David

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.

Matthias Noback

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.

Grigor Yosifov

I have an app with many dynamic subdomains, generated from the DB. I want to stop certain routes for some but not all of them. Therefore I can't use the {dynamic} logic, nor the "host:" logic because it is not excluding, but including. I mean I want to check if I am in a specific subdomain and if I am then to say to router "hay, don't load this yml". Maybe there is workaround for this but I can get to it.

luis

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.

Matthias Noback

Hi Luis,
It is still a pull request but the answer to your question lies here: https://github.com/matthias...

You will need to extend from the base Loader, which allows you to import other resources from within your own loader.

Good luck!

Rob

I got this working - thanks for this post.
The one issue I had was the ExtraLoader::setResolver() must be compatible with ....

To solve the issue make sure to change the
use Symfony\Component\Config\Loader\LoaderResolver declaration at BeSimple\SoapBundle\ServiceDefinition\Loader\AnnotationClassLoader to
use Symfony\Component\Config\Loader\LoaderResolverInterface

Thanks

Matthias Noback

That's right!

Can Berkol

Hi Matthias; great article. I have a quick question. It seems like few thing changed in version 2.3. I applied this example in earlier versions with great success but with 2.3 I need to send a requirements parameter to Route constructor as well. Otherwise I get Route not found error. Do you have any idea why this is happening?

http://symfony.com/doc/curr...

In the code located in above page all I want to achieve is to build the router without $requirements parameter.

$pattern = '/extra/{parameter}';
$defaults = array(
'_controller' => 'AcmeDemoBundle:Demo:extra',
);
$requirements = array(
'parameter' => '\d+',
);
$route = new Route($pattern, $defaults, $requirements);

but without $requirements route cannot be found..

Matthias Noback

Well, I don't know exactly how the router works, but it seems to me that if you have a dynamic parameter in your route, you will always have to define what it can be (so that the router know which URIs match and which don't). If the parameter can really be anything, your requirement would be .+ which means at least one thing of anything ;).

Can Berkol

Matthias thanks for the quick response but I think I was wrong; the route is not being registered at all. I was debugging Symfony components but no luck so far. When I manually enter route definition to app/config things work fine. Anyways, I need to move on for now but as soon as I figure out what's going on I'll let everyone now.

Marcel

I just had to deal with this problem and decided to publish it within a bundle, since it still is an issue. Maybe it will be of help to someone: https://github.com/eschmar/doctrine-routing-bundle (symfony2.4).

Jesus

i have a error! this code :
return $this->redirect($this->generateUrl('acme_hello_homepage',array('name'=>$nombre)));

error generate:
FatalErrorException: Error: Cannot use object of type Acme\TaskBundle\Entity\Task as array in /var/www/Symfony/src/Acme/TaskBundle/Controller/DefaultController.php line 31

Symfson

I tried using the code exactly as written above and it fails with the following error:

FileLoaderLoadException: Cannot load resource ".".

Symfson

Solved :

php app/console generate:bundle --namespace=Acme/RoutingBundle

or

php bin/console generate:bundle --namespace=Acme/RoutingBundle

Tomáš Votruba

Just a tip to remove routing.yml custom configuration.

Use extension and prepend() method:


/**
* {@inheritdoc}
*/
public function prepend(ContainerBuilder $containerBuilder)
{
$containerBuilder->prependExtensionConfig('routing.' . __CLASS__, [
'resource' => '.',
'type' => 'extra'
]);
}

eliotik

Hi, why did you use'routing.' . __CLASS__, ? It will create the key in extensionsConfig with the name something like routing.App\Bundle\Class with array passed as second argument, but this do nothing, symfony dont know what to do with this and custom loader not triggered to load resources. I like you idea to add dynamicaly on prepend this config but how to do it correctly?

may be it should be like:

public function prepend(ContainerBuilder $containerBuilder)
{
$containerBuilder->prependExtensionConfig('your_bundle_name', ['routing' => [
'resource' => '.',
'type' => 'extra'
]]);
}

?
Thank you.

Tomáš Votruba

Just to make unique id and reusable code in other extensions.
Using 'your_bundle_name' would require writing new and new custom names with no added value.

eliotik

Thank you for your reply, but i tried your code, and symfony do nothing

Tomáš Votruba

It's been a long time I used this and I'm not sure the required conditions.

I think you might need to have in config after all.

AcmeRoutingBundle:
resource: .
type: extra

Tomáš Votruba

I've tried this now and it's still broken. I made it work probably in some other way. So sorry for confusion.

I'm working on some easier solution at the moment. Just keep following this repository: https://github.com/Symplify...

Tomáš Votruba
Tomáš Votruba

Here is a solution in a package for Symfony 2.8/3.0: https://github.com/Symplify...

Tomáš Votruba

And the article about that http://www.tomasvotruba.cz/...

Thanks for consultation btw :) the tag was really missed!