1
votes

I have some Controllers defined as services and I need to have the classname of my controllers from the route name.

For non-service controllers I can get the route collection with the router service:

$route = $this->router->getRouteCollection()->get($routeName);
//Retrieve an object like that:

Route {
  -path: "/admin/dashboard"
  -host: ""
  -schemes: []
  -methods: []
  -defaults: array:1 [
    "_controller" => "AppBundle\Controller\Admin\AdminController::dashboardAction"
  ]
  -requirements: []
  -options: array:1 []
  -compiled: null
  -condition: ""
}

I can access the controller classname with $route["defaults"]["_controller"] so this is fine.

The issue is with my controllers as services, the _controller attribute is the name of the service, not the Controller class (like app.controller.admin.user:listAction) I have the name of the service but I need to have the classname (AppBundle\Controller\Admin\UserController)

The only solution I came up with is to get the service from the Container and use get_class() on the service but it will have a huge performance impact only to retrieve the class of the controller/service.

Is there any other solution ?

1
I believe there aren't really any other alternatives that would be more performant. What do you need to do with the classname? - Gerry
I am trying to reproduce this tutorial: trisoft.ro/blog/6-symfony2-advanced-menus and I need the className in order to read metadata: $this->metadataReader->loadMetadataForClass(new \ReflectionClass($class)); - iBadGamer
I suppose you could add a _controller_classname parameter to your routes. But needing the controller class name to generate menus does not seem like the ideal design. - Cerad
All I want to do is to set authorization in only one place. If I restrict the reporting part of my administration website to one role, I do not want to duplicate this configuration in order to hide the menu for people who does not have access. The solution above allows me to use the @Security annotation and to use this information in order to hide specific pieces of my menu. - iBadGamer
I think calling the service and calling get_class() is the way to go, but you might add a caching layer on top of this. - Gerry

1 Answers

0
votes

as suggested in https://github.com/FriendsOfSymfony/FOSUserBundle/issues/2751, I implemented a cached map to get route-names resolved to controllers classes and methods.

<?php
// src/Cache/RouteClassMapWarmer.php
namespace App\Cache;

use Symfony\Component\Cache\Simple\PhpFilesCache;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Routing\RouterInterface;

class RouteClassMapWarmer implements CacheWarmerInterface
{
    /** @var ContainerInterface */
    protected $container;
    /** @var RouterInterface */
    protected $router;

    public function __construct(ContainerInterface $container, RouterInterface $router)
    {
        $this->container = $container;
        $this->router = $router;
    }

    public function warmUp($cacheDirectory)
    {
        $cache = new PhpFilesCache('route_class_map', 0, $cacheDirectory);
        $controllers = [];
        foreach ($this->router->getRouteCollection() as $routeName => $route) {
            $controller = $route->getDefault('_controller');
            if (false === strpos($controller, '::')) {
                list($controllerClass, $controllerMethod) = explode(':', $controller, 2);
                // service_id gets resolved here
                $controllerClass = get_class($this->container->get($controllerClass));
            }
            else {
                list($controllerClass, $controllerMethod) = explode('::', $controller, 2);
            }
            $controllers[$routeName] = ['class' => $controllerClass, 'method' => $controllerMethod];
        }
        unset($controller);
        unset($route);
        $cache->set('route_class_map', $controllers);
    }

    public function isOptional()
    {
        return false;
    }
}

And in my RouteHelper, the implementation reading this looks like this

    $cache = new PhpFilesCache('route_class_map', 0, $this->cacheDirectory);
    $controllers = $cache->get('route_class_map');
    if (!isset($controllers[$routeName])) {
        throw new CacheException('No entry for route ' . $routeName . ' forund in RouteClassMap cache, please warmup first.');
    }

    if (null !== $securityAnnotation = $this->annotationReader->getMethodAnnotation((new \ReflectionClass($controllers[$routeName]['class']))->getMethod($controllers[$routeName]['method']), Security::class))
    {
        return $this->securityExpressionHelper->evaluate($securityAnnotation->getExpression(), ['myParameter' => $myParameter]);
    }

This should be much faster than getting the routeCollection and resolve the service_id:method notated _controller-properties against the container on every request.