[FrameworkBundle][HttpKernel] Provide intuitive error message when a controller fails because it's not registered as a service

This commit is contained in:
Simeon Kolev 2019-03-14 14:19:23 +02:00 committed by Fabien Potencier
parent 59e63805a3
commit fbfc623b72
5 changed files with 231 additions and 4 deletions

View File

@ -23,5 +23,10 @@
<argument type="service" id="debug.argument_resolver.inner" />
<argument type="service" id="debug.stopwatch" />
</service>
<service id="argument_resolver.not_tagged_controller" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver">
<tag name="controller.argument_value_resolver" priority="-200" />
<argument />
</service>
</services>
</container>

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Provides an intuitive error message when controller fails because it is not registered as a service.
*
* @author Simeon Kolev <simeon.kolev9@gmail.com>
*/
final class NotTaggedControllerValueResolver implements ArgumentValueResolverInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function supports(Request $request, ArgumentMetadata $argument)
{
$controller = $request->attributes->get('_controller');
if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) {
$controller = $controller[0].'::'.$controller[1];
} elseif (!\is_string($controller) || '' === $controller) {
return false;
}
if ('\\' === $controller[0]) {
$controller = ltrim($controller, '\\');
}
if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) {
$controller = substr($controller, 0, $i).strtolower(substr($controller, $i));
}
return false === $this->container->has($controller);
}
/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument)
{
if (\is_array($controller = $request->attributes->get('_controller'))) {
$controller = $controller[0].'::'.$controller[1];
}
if ('\\' === $controller[0]) {
$controller = ltrim($controller, '\\');
}
if (!$this->container->has($controller)) {
$i = strrpos($controller, ':');
$controller = substr($controller, 0, $i).strtolower(substr($controller, $i));
}
$what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller);
$message = sprintf('Could not resolve %s, maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?', $what);
throw new RuntimeException($message);
}
}

View File

@ -34,17 +34,19 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
private $resolverServiceId;
private $controllerTag;
private $controllerLocator;
private $notTaggedControllerResolverServiceId;
public function __construct(string $resolverServiceId = 'argument_resolver.service', string $controllerTag = 'controller.service_arguments', string $controllerLocator = 'argument_resolver.controller_locator')
public function __construct(string $resolverServiceId = 'argument_resolver.service', string $controllerTag = 'controller.service_arguments', string $controllerLocator = 'argument_resolver.controller_locator', string $notTaggedControllerResolverServiceId = 'argument_resolver.not_tagged_controller')
{
$this->resolverServiceId = $resolverServiceId;
$this->controllerTag = $controllerTag;
$this->controllerLocator = $controllerLocator;
$this->notTaggedControllerResolverServiceId = $notTaggedControllerResolverServiceId;
}
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition($this->resolverServiceId)) {
if (false === $container->hasDefinition($this->resolverServiceId) && false === $container->hasDefinition($this->notTaggedControllerResolverServiceId)) {
return;
}
@ -181,8 +183,17 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
}
}
$container->getDefinition($this->resolverServiceId)
->replaceArgument(0, $controllerLocatorRef = ServiceLocatorTagPass::register($container, $controllers));
$controllerLocatorRef = ServiceLocatorTagPass::register($container, $controllers);
if ($container->hasDefinition($this->resolverServiceId)) {
$container->getDefinition($this->resolverServiceId)
->replaceArgument(0, $controllerLocatorRef);
}
if ($container->hasDefinition($this->notTaggedControllerResolverServiceId)) {
$container->getDefinition($this->notTaggedControllerResolverServiceId)
->replaceArgument(0, $controllerLocatorRef);
}
$container->setAlias($this->controllerLocator, (string) $controllerLocatorRef);
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
class NotTaggedControllerValueResolverTest extends TestCase
{
public function testDoSupportWhenControllerDoNotExists()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => 'my_controller']);
$this->assertTrue($resolver->supports($request, $argument));
}
public function testDoNotSupportWhenControllerExists()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([
'App\\Controller\\Mine::method' => function () {
return new ServiceLocator([
'dummy' => function () {
return new \stdClass();
},
]);
},
]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']);
$this->assertFalse($resolver->supports($request, $argument));
}
public function testDoNotSupportEmptyController()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => '']);
$this->assertFalse($resolver->supports($request, $argument));
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?
*/
public function testController()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']);
$this->assertTrue($resolver->supports($request, $argument));
$resolver->resolve($request, $argument);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?
*/
public function testControllerWithATrailingBackSlash()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => '\\App\\Controller\\Mine::method']);
$this->assertTrue($resolver->supports($request, $argument));
$resolver->resolve($request, $argument);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?
*/
public function testControllerWithMethodNameStartUppercase()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::Method']);
$this->assertTrue($resolver->supports($request, $argument));
$resolver->resolve($request, $argument);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?
*/
public function testControllerNameIsAnArray()
{
$resolver = new NotTaggedControllerValueResolver(new ServiceLocator([]));
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
$request = $this->requestWithAttributes(['_controller' => ['App\\Controller\\Mine', 'method']]);
$this->assertTrue($resolver->supports($request, $argument));
$resolver->resolve($request, $argument);
}
private function requestWithAttributes(array $attributes)
{
$request = Request::create('/');
foreach ($attributes as $name => $value) {
$request->attributes->set($name, $value);
}
return $request;
}
}

View File

@ -376,6 +376,19 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']);
$this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]);
}
public function testNotTaggedControllerServiceReceivesLocatorArgument()
{
$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.not_tagged_controller')->addArgument([]);
$pass = new RegisterControllerArgumentLocatorsPass();
$pass->process($container);
$locatorArgument = $container->getDefinition('argument_resolver.not_tagged_controller')->getArgument(0);
$this->assertInstanceOf(Reference::class, $locatorArgument);
}
}
class RegisterTestController