0

I need to build a route which has a dynamic condition.

For the moment, I simply use requirements in order the match against a static list a words:

/**
 * @Route(
 *     "/{category}/{id}",
 *     requirements={
 *         "category"="^(foo|bar)$"
 *     }
 * )
 *
 * ...
 */

But now I need these words to be retrieved dynamically from a service method.

While searching a solution, I gave a hope to the condition expression language; but the only variables which are accessible here are the context and the request. However, to achieve my goal I need a full access to container services.

In other words, I would like the following pseudo-php to be executed in order to test the route:

if (in_array($category, MyService::getAllCategories())) {
    /* Inform Symfony that the route matches (then use this controller) */
} else {
    /* Inform Symfony that the route does not match and that the routing process
     * has to go on. */
}

Please note that the main reason of my problem is that the {category} parameter is placed early in the url, and then can offuscate other routes. Then I can't just test my condition inside the controller and return a 404 if the condition is not required. I surely could place this route at the end in the routing process order, but I don't think it is a good solution.

yolenoyer
  • 8,797
  • 2
  • 27
  • 61
  • "my problem is that the {category} parameter is placed early in the url, and then can offuscate other routes", sounds like you use the wrong routing approach ... ? are those other routes variable at that position too? if so, you're very likely to have some collision anyway at some point. if not, put those routes before your categorized routes. – Jakumi Jun 06 '19 at 15:53
  • 1
    Custom route loader can be a solution ... https://symfony.com/doc/current/routing/custom_route_loader.html This example generates not dinamic routes, but works fine. – SilvioQ Jun 06 '19 at 17:03
  • @SilvioQ: indeed, I finally solved my issue by using custom route loaders – yolenoyer Jun 07 '19 at 10:12

2 Answers2

1

... parameter is placed early in the url, and then can offuscate other routes.....

Although bit above confuses me and I hope I didn't misunderstand, here is what you need. At least that is what I know!

  1. You need a custom annotation class. e.g. namespace App\Annotation:Category
  2. Your class above will accept parameters coming from your custom annotation entry. e.g. @Category
  3. Your custom event listener will wire both of them together to make it work. e.g. namespace App\Event\Listener:CategoryAnnotationListener

This is a full example that covers both method and class level custom annotations. Seems like you only need method level so here is your example. Refactor as per your need. Note: Tested and it works.

Usage

declare(strict_types=1);

namespace App\Controller;

use App\Annotation\Category;

/**
 * @Route("/{category}/{id}")
 * @Category
 */
public function index...

Category

namespace App\Annotation;

/**
 * @Annotation
 */
class Category
{
}

Listener

declare(strict_types=1);

namespace App\Event\Listener;

use App\Annotation\Category;
use Doctrine\Common\Annotations\Reader;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class CategoryAnnotationListener
{
    private $annotationReader;

    public function __construct(Reader $annotationReader)
    {
        $this->annotationReader = $annotationReader;
    }

    public function onKernelController(FilterControllerEvent $event): void
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        $controllers = $event->getController();
        if (!is_array($controllers)) {
            return;
        }

        $this->handleAnnotation($controllers, $event->getRequest()->getPathInfo());
    }

    private function handleAnnotation(iterable $controllers, string $path = null): void
    {
        list($controller, $method) = $controllers;

        try {
            $controller = new ReflectionClass($controller);
        } catch (ReflectionException $e) {
            throw new RuntimeException('Failed to read annotation!');
        }

        $method = $controller->getMethod($method);
        $annotation = $this->annotationReader->getMethodAnnotation($method, Category::class);

        if ($annotation instanceof Category) {
            $this->doYourThing($path);
        }
    }

    private function doYourThing(string $path = null): void
    {
        // Explode $path to extract "category" and "id"
        // Run your logic against MyService::getAllCategories()
        // Depending on the outcome either throw exception or just return 404
    }
}

Config

services:
    App\Event\Listener\CategoryAnnotationListener:
        tags:
            - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
BentCoder
  • 12,257
  • 22
  • 93
  • 165
1

Custom route loader can be a solution ... http://symfony.com/doc/current/routing/custom_route_loader.html This example generates not dinamic routes, but works fine.

Only as example, assuming CategoryProvider and Category are your classes ...

<?php

// src/Routing/CategoryLoader.php
namespace App\Routing;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use App\CategoryProvider;

class CategoryLoader extends Loader
{
    private $isLoaded = false;

    private $catProvider;

    public function __construct(CategoryProvider $catProvider)
    {
        $this->catProvider = $catProvider;
    }

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

        $routes = new RouteCollection();

        foreach ($this->catProvider->getAll() as $cat) {

            // prepare a new route
            $path = sprintf('/%s/{id}', $cat->getSlug());
            $defaults = [
                '_controller' => 'App\Controller\ExtraController::extra',
            ];
            $requirements = [
                'parameter' => '\d+',
            ];
            $route = new Route($path, $defaults, $requirements);

            // add the new route to the route collection
            $routeName = 'categoryRoute' . $cat->getSlug();
            $routes->add($routeName, $route);

        }

        $this->isLoaded = true;

        return $routes;
    }

    public function supports($resource, $type = null)
    {
        return 'extra' === $type;
    }
}
SilvioQ
  • 1,972
  • 14
  • 26