ARCJSCHMIDT.de
Thoughts from a developer//entrepreneur.
Founder of AETROS.com

open-source

php|Propel1 php|Propel2 php|CDS php|PHP-PM php|php-pm-httpkernel web|Jarves.io web|css-element-queries js|jQuery-selectBox php|PropelBundle php|Propel Sandbox js|angular2-localStorage js|angular-es6-annotations php|php-rest-service php|topsort.php xxx|BetterQuitJobBundle js|angular-typescript-decorators c++|node-core-audio php|optimistic-locking-behavior php|change-logger-behavior

github.com/marcj twitter.com/MarcJSchmidt plus.google.com/+MarcJSchmidt RSS xing.com/profile/MarcJ_Schmidt

Symfony custom/dynamic router.

30 November 2013, by Marc

Adding a real dynamic router in Symfony through a Bundle is unfortunately not that easy to achieve. Beside the magic in the CMF RouterBundle you could use your classic config files (which is not dynamic) or a Route Loader which has some drawbacks (result is always internally cached) and isn’t really dynamic either.

So, we have actual only the CMF Router as choice when you google for it.

In my opinion it’s a bit too complicated and needs in addition to that (which is a no-go in my case) also a config entry where we have to activate it first. I wanted to have just a router that gets activated as soon as my bundle has been activated in the AppKernel.

Solution: Kernel.request-Event

So, the best and most elegant solution I’ve found is to use the kernel.request event and build a own router that listens on that event.

What you have to do is to create a service that defines your listener class and use there your custom/dynamic router.

1. Define Service

# file: Acme/Resources/config/services.yml

parameters:
     acme.dynamicRouter.listener.class: Acme\EventListener\DynamicRouterListener

services:
     acme.dynamicRouter.listener:
        class: %acme.dynamicRouter.listener.class%
        tags:
            - { name: kernel.event_listener, event: kernel.request, priority: 33 }

Note: We have to set here the priority of 33 because we want to override the default router which has the priority of 32.

2. Create your listener class

<?php
#file: Acme\EventListener\DynamicRouterListener.php

namespace Acme\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

class DynamicRouterListener extends RouterListener
{
    /**
     * @var RouteCollection
     */
    protected $routes;

    function __construct()
    {
        $this->routes = new RouteCollection();
        parent::__construct(
            new UrlMatcher($this->routes, new RequestContext())
        );

        $this->loadRoutes();
    }

    protected function loadRoutes()
    {
        $this->routes->add(
            'dynamic_route_' . ($this->routes->count() + 1),
            new Route(
                'dynamic/bla'
                $defaults = [],
                $requirements = []
            )
        );

        //add another
        //or execute a db query and add multiple routes
        //etc.
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        try {
            parent::onKernelRequest($event);
        } catch(NotFoundHttpException $e) {
        }
    }
}

We overwrite with our DynamicListener the RouterListener class to have the same functionality as the default router, but overwrite here the onKernelRequest because per default the RouterListener throws a NotFoundHttpException exception when there’s no route that matches the current requested path and terminates with this the application. This is actually not what we usually want.

Since I only wanted to have the dynamic router on-top of the default router, so in addition to all routes defined through the classic ways (config, annotation, etc), I kill the NotFoundHttpException exception through the try-catch when my router hasn’t created a route that matches.

If you want to terminate the application if there’s no route that matches you can of course do it like the classic router it does by removing the onKernelRequest completely.

3. Load your services.yml

To get your services.yml loaded you have to add a DependencyInjection Extension:

<?php
#file: Acme\DependencyInjection\AcmeExtension.php

namespace Acme\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class AcmeExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}


That’s it. No you’re able to define real dynamic routes without using the “magic” CMF Router or other hacks.

Perhaps one last Hint:

If you need the Request object (because you need the current URI or something) to generate your routes, you can move the method call of loadRoutes to onKernelRequest and use something like that:

<?php

    protected $routesLoaded = false;

    // ...

    protected function loadRoutes(Request $request)
    {
        // ...
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        if (false === $this->routesLoaded) {
            $this->loadRoutes($event->getRequest());
            $this->routesLoaded = true;
        }

        // ...
    }