Skip to content

Commit

Permalink
[FrameworkBundle][HttpKernel] Provide intuitive error message when a …
Browse files Browse the repository at this point in the history
…controller fails because it's not registered as a service
  • Loading branch information
moynzzz authored and fabpot committed Mar 31, 2019
1 parent 59e6380 commit fbfc623
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml
Expand Up @@ -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>
@@ -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);
}
}
Expand Up @@ -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;
}

Expand Down Expand Up @@ -181,8 +183,17 @@ public function process(ContainerBuilder $container)
}
}

$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);
}
Expand Down
@@ -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;
}
}
Expand Up @@ -376,6 +376,19 @@ public function testBindingsOnChildDefinitions()
$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
Expand Down

0 comments on commit fbfc623

Please sign in to comment.