feature #26085 Deprecate bundle:controller:action and service:method notation (Tobion)

This PR was squashed before being merged into the 4.1-dev branch (closes #26085).

Discussion
----------

Deprecate bundle:controller:action and service:method notation

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #25910
| License       | MIT
| Doc PR        |

The `a::b` notation had some awkward limitations. It supported `MyControllerClass::method` where `MyControllerClass` is either plain class or a service with the same name but the class must exists. This meant it did NOT support `my_service_controller_id::method` because the `class_exists` check would fail at the wrong point in time. But it did support services where class name == id, i.e. the new auto registration based psr naming. This made it very confusing.

I enhanced the `a::b` notation to be very straight forward:
- if `a` exists as a service then use `a` as a service
- otherwise try to use `a` as a class, i.e. `new $a()`
- otherwise check if a::b is a static method (only relevant when the class is abstract or has private contructor). this was potentially supported when using array controller syntax. it now works the same when using the `::` string syntax, like in php itself. since it only happens when nothing else works, it does not have any performance impact.

The old `a:b` syntax is deprecated and just forwards to `a::b` now internally, just as bundle:controller:action.
In general I was able to refactor the logic quite a bit because it always goes through `instantiateController` now.
Spotting deprecated usages is very easy as all outdated routing configs will trigger a deprecation with the DelegatingLoader and it will be normalized in the dumped routes. So you don't get a deprecation again in the ControllerResolver. But if the controller does not come from routing, e.g. twigs render controller function, then it will still be triggered there.

- [x] deprecate `a🅱️c`
- [x] deprecate `a:b`
- [x] update existing references to `a::b`
- [x] fix tests
- [x] fix/add support for static controllers
- [x] add support for closures as controllers
- [x] update Symfony\Component\Routing\Loader\ObjectRouteLoader
- [x] deprecate \Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser but we still need to use it in several places for BC.
- [x] add changelog/upgrade
- [x] update controller.service_arguments logic for double colon controller syntax

Commits
-------

f8a609cdbd Deprecate bundle:controller:action and service:method notation
This commit is contained in:
Fabien Potencier 2018-02-21 06:14:06 +01:00
commit 6651087a98
40 changed files with 507 additions and 463 deletions

View File

@ -15,6 +15,38 @@ EventDispatcher
FrameworkBundle
---------------
* Deprecated `bundle:controller:action` and `service:action` syntaxes to reference controllers. Use `serviceOrFqcn::method`
instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller.
Before:
```yml
bundle_controller:
path: /
defaults:
_controller: FrameworkBundle:Redirect:redirect
service_controller:
path: /
defaults:
_controller: app.my_controller:myAction
```
After:
```yml
bundle_controller:
path: /
defaults:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
service_controller:
path: /
defaults:
_controller: app.my_controller::myAction
```
* Deprecated `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`
* A `RouterInterface` that does not implement the `WarmableInterface` is deprecated.
* The `RequestDataCollector` class has been deprecated. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead.

View File

@ -14,6 +14,38 @@ EventDispatcher
FrameworkBundle
---------------
* Removed support for `bundle:controller:action` and `service:action` syntaxes to reference controllers. Use `serviceOrFqcn::method`
instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller.
Before:
```yml
bundle_controller:
path: /
defaults:
_controller: FrameworkBundle:Redirect:redirect
service_controller:
path: /
defaults:
_controller: app.my_controller:myAction
```
After:
```yml
bundle_controller:
path: /
defaults:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
service_controller:
path: /
defaults:
_controller: app.my_controller::myAction
```
* Removed `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`
* Using a `RouterInterface` that does not implement the `WarmableInterface` is not supported anymore.
* The `RequestDataCollector` class has been removed. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead.

View File

@ -11,6 +11,9 @@ CHANGELOG
* Using a `RouterInterface` that does not implement the `WarmableInterface` is deprecated.
* The `RequestDataCollector` class has been deprecated. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead.
* The `RedirectController` class allows for 307/308 HTTP status codes
* Deprecated `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead where `serviceOrFqcn`
is either the service ID or the FQCN of the controller.
* Deprecated `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`
4.0.0
-----

View File

@ -12,16 +12,13 @@
namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Routing\Route;
/**
* A console command for retrieving information about routes.
@ -83,20 +80,13 @@ EOF
throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name));
}
$callable = $this->extractCallable($route);
$helper->describe($io, $route, array(
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'name' => $name,
'output' => $io,
'callable' => $callable,
));
} else {
foreach ($routes as $route) {
$this->convertController($route);
}
$helper->describe($io, $routes, array(
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
@ -105,41 +95,4 @@ EOF
));
}
}
private function convertController(Route $route)
{
if ($route->hasDefault('_controller')) {
$nameParser = new ControllerNameParser($this->getApplication()->getKernel());
try {
$route->setDefault('_controller', $nameParser->build($route->getDefault('_controller')));
} catch (\InvalidArgumentException $e) {
}
}
}
private function extractCallable(Route $route)
{
if (!$route->hasDefault('_controller')) {
return;
}
$controller = $route->getDefault('_controller');
if (1 === substr_count($controller, ':')) {
list($service, $method) = explode(':', $controller);
try {
return sprintf('%s::%s', get_class($this->getApplication()->getKernel()->getContainer()->get($service)), $method);
} catch (ServiceNotFoundException $e) {
}
}
$nameParser = new ControllerNameParser($this->getApplication()->getKernel());
try {
$shortNotation = $nameParser->build($controller);
$route->setDefault('_controller', $shortNotation);
return $controller;
} catch (\InvalidArgumentException $e) {
}
}
}

View File

@ -95,9 +95,6 @@ class TextDescriptor extends Descriptor
array('Defaults', $this->formatRouterConfig($route->getDefaults())),
array('Options', $this->formatRouterConfig($route->getOptions())),
);
if (isset($options['callable'])) {
$tableRows[] = array('Callable', $options['callable']);
}
$table = new Table($this->getOutput());
$table->setHeaders($tableHeaders)->setRows($tableRows);

View File

@ -19,6 +19,8 @@ use Symfony\Component\HttpKernel\KernelInterface;
* (Bundle\BlogBundle\Controller\PostController::indexAction).
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.1
*/
class ControllerNameParser
{
@ -41,6 +43,10 @@ class ControllerNameParser
*/
public function parse($controller)
{
if (2 > func_num_args() || func_get_arg(1)) {
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED);
}
$parts = explode(':', $controller);
if (3 !== count($parts) || in_array('', $parts, true)) {
throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "a:b:c" controller string.', $controller));
@ -86,6 +92,8 @@ class ControllerNameParser
*/
public function build($controller)
{
@trigger_error(sprintf('The %s class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED);
if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) {
throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "class::method" string.', $controller));
}

View File

@ -37,16 +37,13 @@ class ControllerResolver extends ContainerControllerResolver
{
if (false === strpos($controller, '::') && 2 === substr_count($controller, ':')) {
// controller in the a:b:c notation then
$controller = $this->parser->parse($controller);
$deprecatedNotation = $controller;
$controller = $this->parser->parse($deprecatedNotation, false);
@trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1. Use %s instead.', $deprecatedNotation, $controller), E_USER_DEPRECATED);
}
$resolvedController = parent::createController($controller);
if (1 === substr_count($controller, ':') && is_array($resolvedController)) {
$resolvedController[0] = $this->configureController($resolvedController[0]);
}
return $resolvedController;
return parent::createController($controller);
}
/**

View File

@ -20,6 +20,8 @@ use Symfony\Component\HttpKernel\KernelEvents;
* Guarantees that the _controller key is parsed into its final format.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @deprecated since Symfony 4.1
*/
class ResolveControllerNameSubscriber implements EventSubscriberInterface
{
@ -35,7 +37,7 @@ class ResolveControllerNameSubscriber implements EventSubscriberInterface
$controller = $event->getRequest()->attributes->get('_controller');
if (is_string($controller) && false === strpos($controller, '::') && 2 === substr_count($controller, ':')) {
// controller in the a:b:c notation then
$event->getRequest()->attributes->set('_controller', $this->parser->parse($controller));
$event->getRequest()->attributes->set('_controller', $this->parser->parse($controller, false));
}
}

View File

@ -64,7 +64,7 @@ trait MicroKernelTrait
$loader->load(function (ContainerBuilder $container) use ($loader) {
$container->loadFromExtension('framework', array(
'router' => array(
'resource' => 'kernel:loadRoutes',
'resource' => 'kernel::loadRoutes',
'type' => 'service',
),
));

View File

@ -73,14 +73,29 @@ class DelegatingLoader extends BaseDelegatingLoader
}
foreach ($collection->all() as $route) {
if (!is_string($controller = $route->getDefault('_controller')) || !$controller) {
if (!is_string($controller = $route->getDefault('_controller'))) {
continue;
}
try {
$controller = $this->parser->parse($controller);
} catch (\InvalidArgumentException $e) {
// unable to optimize unknown notation
if (false !== strpos($controller, '::')) {
continue;
}
if (2 === substr_count($controller, ':')) {
$deprecatedNotation = $controller;
try {
$controller = $this->parser->parse($controller, false);
@trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1. Use %s instead.', $deprecatedNotation, $controller), E_USER_DEPRECATED);
} catch (\InvalidArgumentException $e) {
// unable to optimize unknown notation
}
}
if (1 === substr_count($controller, ':')) {
$controller = str_replace(':', '::', $controller);
@trigger_error(sprintf('Referencing controllers with a single colon is deprecated since Symfony 4.1. Use %s instead.', $controller), E_USER_DEPRECATED);
}
$route->setDefault('_controller', $controller);

View File

@ -16,6 +16,9 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\HttpKernel\Kernel;
/**
* @group legacy
*/
class ControllerNameParserTest extends TestCase
{
protected $loader;

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Tests\Controller\ContainerControllerResolverTest;
class ControllerResolverTest extends ContainerControllerResolverTest
@ -32,6 +33,7 @@ class ControllerResolverTest extends ContainerControllerResolverTest
$controller = $resolver->getController($request);
$this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller[0]);
$this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller[0]->getContainer());
$this->assertSame('testAction', $controller[1]);
}
@ -48,6 +50,10 @@ class ControllerResolverTest extends ContainerControllerResolverTest
$this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller->getContainer());
}
/**
* @group legacy
* @expectedDeprecation Referencing controllers with FooBundle:Default:test is deprecated since Symfony 4.1. Use Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction instead.
*/
public function testGetControllerWithBundleNotation()
{
$shortName = 'FooBundle:Default:test';
@ -81,7 +87,7 @@ class ControllerResolverTest extends ContainerControllerResolverTest
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', TestAbstractController::class.':testAction');
$request->attributes->set('_controller', TestAbstractController::class.'::testAction');
$this->assertSame(array($controller, 'testAction'), $resolver->getController($request));
$this->assertSame($container, $controller->getContainer());
@ -117,7 +123,7 @@ class ControllerResolverTest extends ContainerControllerResolverTest
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', DummyController::class.':fooAction');
$request->attributes->set('_controller', DummyController::class.'::fooAction');
$this->assertSame(array($controller, 'fooAction'), $resolver->getController($request));
$this->assertSame($container, $controller->getContainer());
@ -157,13 +163,13 @@ class ControllerResolverTest extends ContainerControllerResolverTest
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', DummyController::class.':fooAction');
$request->attributes->set('_controller', DummyController::class.'::fooAction');
$this->assertSame(array($controller, 'fooAction'), $resolver->getController($request));
$this->assertSame($controllerContainer, $controller->getContainer());
}
protected function createControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null, ControllerNameParser $parser = null)
protected function createControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null, ControllerNameParser $parser = null): ControllerResolverInterface
{
if (!$parser) {
$parser = $this->createMockParser();

View File

@ -33,7 +33,7 @@ class SubRequestController implements ContainerAwareInterface
// ...to check that the FragmentListener still references the right Request
// when rendering another fragment after the error occurred
// should render en/html instead of fr/json
$content .= $handler->render(new ControllerReference('TestBundle:SubRequest:fragment'));
$content .= $handler->render(new ControllerReference(self::class.'::fragmentAction'));
// forces the LocaleListener to set fr for the locale...
// should render fr/json

View File

@ -24,7 +24,7 @@ class SubRequestServiceResolutionController implements ContainerAwareInterface
public function indexAction()
{
$request = $this->container->get('request_stack')->getCurrentRequest();
$path['_controller'] = 'TestBundle:SubRequestServiceResolution:fragment';
$path['_controller'] = self::class.'::fragmentAction';
$subRequest = $request->duplicate(array(), null, $path);
return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);

View File

@ -1,49 +1,49 @@
session_welcome:
path: /session
defaults: { _controller: TestBundle:Session:welcome }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::welcomeAction }
session_welcome_name:
path: /session/{name}
defaults: { _controller: TestBundle:Session:welcome }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::welcomeAction }
session_logout:
path: /session_logout
defaults: { _controller: TestBundle:Session:logout}
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::logoutAction }
session_setflash:
path: /session_setflash/{message}
defaults: { _controller: TestBundle:Session:setFlash}
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::setFlashAction }
session_showflash:
path: /session_showflash
defaults: { _controller: TestBundle:Session:showFlash}
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::showFlashAction }
profiler:
path: /profiler
defaults: { _controller: TestBundle:Profiler:index }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\ProfilerController::indexAction }
subrequest_index:
path: /subrequest/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:index, _format: "html" }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestController::indexAction, _format: html }
schemes: [https]
subrequest_fragment_error:
path: /subrequest/fragment/error/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:fragmentError, _format: "html" }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestController::fragmentErrorAction, _format: html }
schemes: [http]
subrequest_fragment:
path: /subrequest/fragment/{_locale}.{_format}
defaults: { _controller: TestBundle:SubRequest:fragment, _format: "html" }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestController::fragmentAction, _format: html }
schemes: [http]
fragment_home:
path: /fragment_home
defaults: { _controller: TestBundle:Fragment:index, _format: txt }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::indexAction, _format: txt }
fragment_inlined:
path: /fragment_inlined
defaults: { _controller: TestBundle:Fragment:inlined }
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::inlinedAction }
array_controller:
path: /array_controller

View File

@ -1,4 +1,4 @@
sub_request_page:
path: /subrequest
defaults:
_controller: 'TestBundle:SubRequestServiceResolution:index'
_controller: 'Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestServiceResolutionController::indexAction'

View File

@ -1,14 +1,14 @@
<?php echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:inlined', array(
<?php echo $this->get('actions')->render($this->get('actions')->controller('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::inlinedAction', array(
'options' => array(
'bar' => $bar,
'eleven' => 11,
),
)));
?>--<?php
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customformat', array('_format' => 'html')));
echo $this->get('actions')->render($this->get('actions')->controller('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::customformatAction', array('_format' => 'html')));
?>--<?php
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customlocale', array('_locale' => 'es')));
echo $this->get('actions')->render($this->get('actions')->controller('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::customlocaleAction', array('_locale' => 'es')));
?>--<?php
$app->getRequest()->setLocale('fr');
echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:forwardlocale'));
echo $this->get('actions')->render($this->get('actions')->controller('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::forwardlocaleAction'));
?>

View File

@ -72,8 +72,8 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface
protected function configureRoutes(RouteCollectionBuilder $routes)
{
$routes->add('/', 'kernel:halloweenAction');
$routes->add('/danger', 'kernel:dangerousAction');
$routes->add('/', 'kernel::halloweenAction');
$routes->add('/danger', 'kernel::dangerousAction');
}
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)

View File

@ -23,11 +23,11 @@
"symfony/config": "~3.4|~4.0",
"symfony/event-dispatcher": "~3.4|~4.0",
"symfony/http-foundation": "~3.4|~4.0",
"symfony/http-kernel": "~3.4|~4.0",
"symfony/http-kernel": "^4.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "~3.4|~4.0",
"symfony/finder": "~3.4|~4.0",
"symfony/routing": "^3.4.5|^4.0.5"
"symfony/routing": "^4.1"
},
"require-dev": {
"doctrine/cache": "~1.0",

View File

@ -1,30 +1,30 @@
form_login:
path: /login
defaults: { _controller: CsrfFormLoginBundle:Login:login }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::loginAction }
form_login_check:
path: /login_check
defaults: { _controller: CsrfFormLoginBundle:Login:loginCheck }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::loginCheckAction }
form_login_homepage:
path: /
defaults: { _controller: CsrfFormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_custom_target_path:
path: /foo
defaults: { _controller: CsrfFormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_default_target_path:
path: /profile
defaults: { _controller: CsrfFormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_redirect_to_protected_resource_after_login:
path: /protected-resource
defaults: { _controller: CsrfFormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::afterLoginAction }
form_logout:
path: /logout_path
form_secure_action:
path: /secure-but-not-covered-by-access-control
defaults: { _controller: CsrfFormLoginBundle:Login:secure }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController::secureAction }

View File

@ -1,29 +1,29 @@
localized_login_path:
path: /{_locale}/login
defaults: { _controller: FormLoginBundle:Localized:login }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::loginAction }
requirements: { _locale: "^[a-z]{2}$" }
localized_check_path:
path: /{_locale}/login_check
defaults: { _controller: FormLoginBundle:Localized:loginCheck }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::loginCheckAction }
requirements: { _locale: "^[a-z]{2}$" }
localized_default_target_path:
path: /{_locale}/profile
defaults: { _controller: FormLoginBundle:Localized:profile }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::profileAction }
requirements: { _locale: "^[a-z]{2}$" }
localized_logout_path:
path: /{_locale}/logout
defaults: { _controller: FormLoginBundle:Localized:logout }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::logoutAction }
requirements: { _locale: "^[a-z]{2}$" }
localized_logout_target_path:
path: /{_locale}/
defaults: { _controller: FormLoginBundle:Localized:homepage }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::homepageAction }
requirements: { _locale: "^[a-z]{2}$" }
localized_secure_path:
path: /{_locale}/secure/
defaults: { _controller: FormLoginBundle:Localized:secure }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController::secureAction }
requirements: { _locale: "^[a-z]{2}$" }

View File

@ -1,26 +1,26 @@
form_login:
path: /login
defaults: { _controller: FormLoginBundle:Login:login }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::loginAction }
form_login_check:
path: /login_check
defaults: { _controller: FormLoginBundle:Login:loginCheck }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::loginCheckAction }
form_login_homepage:
path: /
defaults: { _controller: FormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_custom_target_path:
path: /foo
defaults: { _controller: FormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_default_target_path:
path: /profile
defaults: { _controller: FormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::afterLoginAction }
form_login_redirect_to_protected_resource_after_login:
path: /protected_resource
defaults: { _controller: FormLoginBundle:Login:afterLogin }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::afterLoginAction }
highly_protected_resource:
path: /highly_protected_resource
@ -36,7 +36,7 @@ form_logout:
form_secure_action:
path: /secure-but-not-covered-by-access-control
defaults: { _controller: FormLoginBundle:Login:secure }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController::secureAction }
protected-via-expression:
path: /protected-via-expression

View File

@ -1,3 +1,3 @@
login_check:
path: /chk
defaults: { _controller: JsonLoginBundle:Test:loginCheck }
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\Controller\TestController::loginCheckAction }

View File

@ -20,7 +20,7 @@
"ext-xml": "*",
"symfony/security": "~4.1",
"symfony/dependency-injection": "^3.4.3|^4.0.3",
"symfony/http-kernel": "~3.4|~4.0"
"symfony/http-kernel": "^4.1"
},
"require-dev": {
"symfony/asset": "~3.4|~4.0",

View File

@ -34,7 +34,7 @@ class Configuration implements ConfigurationInterface
$rootNode
->children()
->scalarNode('exception_controller')->defaultValue('twig.controller.exception:showAction')->end()
->scalarNode('exception_controller')->defaultValue('twig.controller.exception::showAction')->end()
->end()
;

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_twig_error_test" path="/{code}.{_format}">
<default key="_controller">twig.controller.preview_error:previewErrorPageAction</default>
<default key="_controller">twig.controller.preview_error::previewErrorPageAction</default>
<default key="_format">html</default>
<requirement key="code">\d+</requirement>
</route>

View File

@ -5,43 +5,43 @@
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_profiler_home" path="/">
<default key="_controller">web_profiler.controller.profiler:homeAction</default>
<default key="_controller">web_profiler.controller.profiler::homeAction</default>
</route>
<route id="_profiler_search" path="/search">
<default key="_controller">web_profiler.controller.profiler:searchAction</default>
<default key="_controller">web_profiler.controller.profiler::searchAction</default>
</route>
<route id="_profiler_search_bar" path="/search_bar">
<default key="_controller">web_profiler.controller.profiler:searchBarAction</default>
<default key="_controller">web_profiler.controller.profiler::searchBarAction</default>
</route>
<route id="_profiler_phpinfo" path="/phpinfo">
<default key="_controller">web_profiler.controller.profiler:phpinfoAction</default>
<default key="_controller">web_profiler.controller.profiler::phpinfoAction</default>
</route>
<route id="_profiler_search_results" path="/{token}/search/results">
<default key="_controller">web_profiler.controller.profiler:searchResultsAction</default>
<default key="_controller">web_profiler.controller.profiler::searchResultsAction</default>
</route>
<route id="_profiler_open_file" path="/open">
<default key="_controller">web_profiler.controller.profiler:openAction</default>
<default key="_controller">web_profiler.controller.profiler::openAction</default>
</route>
<route id="_profiler" path="/{token}">
<default key="_controller">web_profiler.controller.profiler:panelAction</default>
<default key="_controller">web_profiler.controller.profiler::panelAction</default>
</route>
<route id="_profiler_router" path="/{token}/router">
<default key="_controller">web_profiler.controller.router:panelAction</default>
<default key="_controller">web_profiler.controller.router::panelAction</default>
</route>
<route id="_profiler_exception" path="/{token}/exception">
<default key="_controller">web_profiler.controller.exception:showAction</default>
<default key="_controller">web_profiler.controller.exception::showAction</default>
</route>
<route id="_profiler_exception_css" path="/{token}/exception.css">
<default key="_controller">web_profiler.controller.exception:cssAction</default>
<default key="_controller">web_profiler.controller.exception::cssAction</default>
</route>
</routes>

View File

@ -5,6 +5,6 @@
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_wdt" path="/{token}">
<default key="_controller">web_profiler.controller.profiler:toolbarAction</default>
<default key="_controller">web_profiler.controller.profiler::toolbarAction</default>
</route>
</routes>

View File

@ -6,6 +6,7 @@ CHANGELOG
* added orphaned events support to `EventDataCollector`
* `ExceptionListener` now logs and collects exceptions at priority `2048` (previously logged at `-128` and collected at `0`)
* Deprecated `service:action` syntax with a single colon to reference controllers. Use `service::method` instead.
4.0.0
-----

View File

@ -14,7 +14,6 @@ namespace Symfony\Component\HttpKernel\Controller;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
/**
* A controller resolver searching for a controller in a psr-11 container when using the "service:method" notation.
@ -33,58 +32,14 @@ class ContainerControllerResolver extends ControllerResolver
parent::__construct($logger);
}
/**
* {@inheritdoc}
*/
public function getController(Request $request)
{
$controller = parent::getController($request);
if (is_array($controller) && isset($controller[0]) && is_string($controller[0]) && $this->container->has($controller[0])) {
$controller[0] = $this->instantiateController($controller[0]);
}
return $controller;
}
/**
* Returns a callable for the given controller.
*
* @param string $controller A Controller string
*
* @return mixed A PHP callable
*
* @throws \LogicException When the name could not be parsed
* @throws \InvalidArgumentException When the controller class does not exist
*/
protected function createController($controller)
{
if (false !== strpos($controller, '::')) {
return parent::createController($controller);
if (1 === substr_count($controller, ':')) {
$controller = str_replace(':', '::', $controller);
@trigger_error(sprintf('Referencing controllers with a single colon is deprecated since Symfony 4.1. Use %s instead.', $controller), E_USER_DEPRECATED);
}
$method = null;
if (1 == substr_count($controller, ':')) {
// controller in the "service:method" notation
list($controller, $method) = explode(':', $controller, 2);
}
if (!$this->container->has($controller)) {
$this->throwExceptionIfControllerWasRemoved($controller);
throw new \LogicException(sprintf('Controller not found: service "%s" does not exist.', $controller));
}
$service = $this->container->get($controller);
if (null !== $method) {
return array($service, $method);
}
if (!method_exists($service, '__invoke')) {
throw new \LogicException(sprintf('Controller "%s" cannot be called without a method name. Did you forget an "__invoke" method?', $controller));
}
return $service;
return parent::createController($controller);
}
/**
@ -98,22 +53,22 @@ class ContainerControllerResolver extends ControllerResolver
try {
return parent::instantiateController($class);
} catch (\ArgumentCountError $e) {
} catch (\Error $e) {
}
$this->throwExceptionIfControllerWasRemoved($class, $e);
throw $e;
if ($e instanceof \ArgumentCountError) {
throw new \InvalidArgumentException(sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define such a service?', $class), 0, $e);
}
throw new \InvalidArgumentException(sprintf('Controller "%s" does neither exist as service nor as class', $class), 0, $e);
}
/**
* @param string $controller
* @param \Exception|\Throwable|null $previous
*/
private function throwExceptionIfControllerWasRemoved($controller, $previous = null)
private function throwExceptionIfControllerWasRemoved(string $controller, \Throwable $previous)
{
if ($this->container instanceof Container && isset($this->container->getRemovedIds()[$controller])) {
throw new \LogicException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $controller), 0, $previous);
throw new \InvalidArgumentException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $controller), 0, $previous);
}
}
}

View File

@ -16,10 +16,10 @@ use Symfony\Component\HttpFoundation\Request;
/**
* This implementation uses the '_controller' request attribute to determine
* the controller to execute and uses the request attributes to determine
* the controller method arguments.
* the controller to execute.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
*/
class ControllerResolver implements ControllerResolverInterface
{
@ -32,9 +32,6 @@ class ControllerResolver implements ControllerResolverInterface
/**
* {@inheritdoc}
*
* This method looks for a '_controller' request attribute that represents
* the controller name (a string like ClassName::MethodName).
*/
public function getController(Request $request)
{
@ -47,23 +44,42 @@ class ControllerResolver implements ControllerResolverInterface
}
if (is_array($controller)) {
if (isset($controller[0]) && is_string($controller[0]) && isset($controller[1])) {
try {
$controller[0] = $this->instantiateController($controller[0]);
} catch (\Error | \LogicException $e) {
try {
// We cannot just check is_callable but have to use reflection because a non-static method
// can still be called statically in PHP but we don't want that. This is deprecated in PHP 7, so we
// could simplify this with PHP 8.
if ((new \ReflectionMethod($controller[0], $controller[1]))->isStatic()) {
return $controller;
}
} catch (\ReflectionException $reflectionException) {
throw $e;
}
throw $e;
}
}
if (!is_callable($controller)) {
throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s', $request->getPathInfo(), $this->getControllerError($controller)));
}
return $controller;
}
if (is_object($controller)) {
if (method_exists($controller, '__invoke')) {
return $controller;
if (!is_callable($controller)) {
throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s', $request->getPathInfo(), $this->getControllerError($controller)));
}
throw new \InvalidArgumentException(sprintf('Controller "%s" for URI "%s" is not callable.', get_class($controller), $request->getPathInfo()));
return $controller;
}
if (false === strpos($controller, ':')) {
if (method_exists($controller, '__invoke')) {
return $this->instantiateController($controller);
} elseif (function_exists($controller)) {
return $controller;
}
if (function_exists($controller)) {
return $controller;
}
$callable = $this->createController($controller);
@ -81,22 +97,28 @@ class ControllerResolver implements ControllerResolverInterface
* @param string $controller A Controller string
*
* @return callable A PHP callable
*
* @throws \InvalidArgumentException
*/
protected function createController($controller)
{
if (false === strpos($controller, '::')) {
throw new \InvalidArgumentException(sprintf('Unable to find controller "%s".', $controller));
return $this->instantiateController($controller);
}
list($class, $method) = explode('::', $controller, 2);
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
}
try {
return array($this->instantiateController($class), $method);
} catch (\Error | \LogicException $e) {
try {
if ((new \ReflectionMethod($class, $method))->isStatic()) {
return $class.'::'.$method;
}
} catch (\ReflectionException $reflectionException) {
throw $e;
}
return array($this->instantiateController($class), $method);
throw $e;
}
}
/**
@ -115,24 +137,25 @@ class ControllerResolver implements ControllerResolverInterface
{
if (is_string($callable)) {
if (false !== strpos($callable, '::')) {
$callable = explode('::', $callable);
}
if (class_exists($callable) && !method_exists($callable, '__invoke')) {
return sprintf('Class "%s" does not have a method "__invoke".', $callable);
}
if (!function_exists($callable)) {
$callable = explode('::', $callable, 2);
} else {
return sprintf('Function "%s" does not exist.', $callable);
}
}
if (!is_array($callable)) {
return sprintf('Invalid type for controller given, expected string or array, got "%s".', gettype($callable));
if (is_object($callable)) {
$availableMethods = $this->getClassMethodsWithoutMagicMethods($callable);
$alternativeMsg = $availableMethods ? sprintf(' or use one of the available methods: "%s"', implode('", "', $availableMethods)) : '';
return sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', get_class($callable), $alternativeMsg);
}
if (2 !== count($callable)) {
return 'Invalid format for controller, expected array(controller, method) or controller::method.';
if (!is_array($callable)) {
return sprintf('Invalid type for controller given, expected string, array or object, got "%s".', gettype($callable));
}
if (!isset($callable[0]) || !isset($callable[1]) || 2 !== count($callable)) {
return 'Invalid array callable, expected array(controller, method).';
}
list($controller, $method) = $callable;
@ -147,7 +170,7 @@ class ControllerResolver implements ControllerResolverInterface
return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className);
}
$collection = get_class_methods($controller);
$collection = $this->getClassMethodsWithoutMagicMethods($controller);
$alternatives = array();
@ -171,4 +194,13 @@ class ControllerResolver implements ControllerResolverInterface
return $message;
}
private function getClassMethodsWithoutMagicMethods($classOrObject)
{
$methods = get_class_methods($classOrObject);
return array_filter($methods, function(string $method) {
return 0 !== strncmp($method, '__', 2);
});
}
}

View File

@ -17,8 +17,6 @@ use Symfony\Component\HttpFoundation\Request;
* A ControllerResolverInterface implementation knows how to determine the
* controller to execute based on a Request object.
*
* It can also determine the arguments to pass to the Controller.
*
* A Controller can be any valid PHP callable.
*
* @author Fabien Potencier <fabien@symfony.com>
@ -37,7 +35,7 @@ interface ControllerResolverInterface
* @return callable|false A PHP callable representing the Controller,
* or false if this resolver is not able to determine the controller
*
* @throws \LogicException If the controller can't be found
* @throws \LogicException If a controller was found based on the request but it is not callable
*/
public function getController(Request $request);
}

View File

@ -168,7 +168,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
}
// register the maps as a per-method service-locators
if ($args) {
$controllers[$id.':'.$r->name] = ServiceLocatorTagPass::register($container, $args);
$controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args);
}
}
}

View File

@ -47,8 +47,7 @@ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface
} else {
// any methods listed for call-at-instantiation cannot be actions
$reason = false;
$action = substr(strrchr($controller, ':'), 1);
$id = substr($controller, 0, -1 - strlen($action));
list($id, $action) = explode('::', $controller);
$controllerDef = $container->getDefinition($id);
foreach ($controllerDef->getMethodCalls() as list($method)) {
if (0 === strcasecmp($action, $method)) {
@ -57,9 +56,9 @@ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface
}
}
if (!$reason) {
if ($controllerDef->getClass() === $id) {
$controllers[$id.'::'.$action] = $argumentRef;
}
// Deprecated since Symfony 4.1. See Symfony\Component\HttpKernel\Controller\ContainerControllerResolver
$controllers[$id.':'.$action] = $argumentRef;
if ('__invoke' === $action) {
$controllers[$id] = $argumentRef;
}

View File

@ -13,15 +13,21 @@ namespace Symfony\Component\HttpKernel\Tests\Controller;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
class ContainerControllerResolverTest extends ControllerResolverTest
{
public function testGetControllerService()
/**
* @group legacy
* @expectedDeprecation Referencing controllers with a single colon is deprecated since Symfony 4.1. Use foo::action instead.
*/
public function testGetControllerServiceWithSingleColon()
{
$service = new ControllerTestService('foo');
$container = $this->createMockContainer();
$container->expects($this->once())
->method('has')
@ -30,22 +36,47 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$container->expects($this->once())
->method('get')
->with('foo')
->will($this->returnValue($this))
->will($this->returnValue($service))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', 'foo:controllerMethod1');
$request->attributes->set('_controller', 'foo:action');
$controller = $resolver->getController($request);
$this->assertInstanceOf(get_class($this), $controller[0]);
$this->assertSame('controllerMethod1', $controller[1]);
$this->assertSame($service, $controller[0]);
$this->assertSame('action', $controller[1]);
}
public function testGetControllerService()
{
$service = new ControllerTestService('foo');
$container = $this->createMockContainer();
$container->expects($this->once())
->method('has')
->with('foo')
->will($this->returnValue(true));
$container->expects($this->once())
->method('get')
->with('foo')
->will($this->returnValue($service))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', 'foo::action');
$controller = $resolver->getController($request);
$this->assertSame($service, $controller[0]);
$this->assertSame('action', $controller[1]);
}
public function testGetControllerInvokableService()
{
$invokableController = new InvokableController('bar');
$service = new InvokableControllerService('bar');
$container = $this->createMockContainer();
$container->expects($this->once())
@ -56,7 +87,7 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$container->expects($this->once())
->method('get')
->with('foo')
->will($this->returnValue($invokableController))
->will($this->returnValue($service))
;
$resolver = $this->createControllerResolver(null, $container);
@ -65,118 +96,72 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$controller = $resolver->getController($request);
$this->assertEquals($invokableController, $controller);
$this->assertSame($service, $controller);
}
public function testGetControllerInvokableServiceWithClassNameAsName()
{
$invokableController = new InvokableController('bar');
$className = __NAMESPACE__.'\InvokableController';
$service = new InvokableControllerService('bar');
$container = $this->createMockContainer();
$container->expects($this->once())
->method('has')
->with($className)
->with(InvokableControllerService::class)
->will($this->returnValue(true))
;
$container->expects($this->once())
->method('get')
->with($className)
->will($this->returnValue($invokableController))
->with(InvokableControllerService::class)
->will($this->returnValue($service))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', $className);
$request->attributes->set('_controller', InvokableControllerService::class);
$controller = $resolver->getController($request);
$this->assertEquals($invokableController, $controller);
}
public function testNonInstantiableController()
{
$container = $this->createMockContainer();
$container->expects($this->once())
->method('has')
->with(NonInstantiableController::class)
->will($this->returnValue(false))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', array(NonInstantiableController::class, 'action'));
$controller = $resolver->getController($request);
$this->assertSame(array(NonInstantiableController::class, 'action'), $controller);
$this->assertSame($service, $controller);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Controller "Symfony\Component\HttpKernel\Tests\Controller\ImpossibleConstructController" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?
* Tests where the fallback instantiation fails due to required constructor arguments.
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?
*/
public function testNonConstructController()
public function testExceptionWhenUsingRemovedControllerServiceWithClassNameAsName()
{
$container = $this->getMockBuilder(Container::class)->getMock();
$container->expects($this->at(0))
$container->expects($this->once())
->method('has')
->with(ImpossibleConstructController::class)
->will($this->returnValue(true))
;
$container->expects($this->at(1))
->method('has')
->with(ImpossibleConstructController::class)
->with(ControllerTestService::class)
->will($this->returnValue(false))
;
$container->expects($this->atLeastOnce())
->method('getRemovedIds')
->with()
->will($this->returnValue(array(ImpossibleConstructController::class => true)))
->will($this->returnValue(array(ControllerTestService::class => true)))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', array(ImpossibleConstructController::class, 'action'));
$request->attributes->set('_controller', array(ControllerTestService::class, 'action'));
$resolver->getController($request);
}
public function testNonInstantiableControllerWithCorrespondingService()
{
$service = new \stdClass();
$container = $this->createMockContainer();
$container->expects($this->atLeastOnce())
->method('has')
->with(NonInstantiableController::class)
->will($this->returnValue(true))
;
$container->expects($this->atLeastOnce())
->method('get')
->with(NonInstantiableController::class)
->will($this->returnValue($service))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', array(NonInstantiableController::class, 'action'));
$controller = $resolver->getController($request);
$this->assertSame(array($service, 'action'), $controller);
}
/**
* @expectedException \LogicException
* Tests where the fallback instantiation fails due to non-existing class.
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Controller "app.my_controller" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?
*/
public function testExceptionWhenUsingRemovedControllerService()
{
$container = $this->getMockBuilder(Container::class)->getMock();
$container->expects($this->at(0))
$container->expects($this->once())
->method('has')
->with('app.my_controller')
->will($this->returnValue(false))
@ -195,65 +180,31 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$resolver->getController($request);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Controller "app.my_controller" cannot be called without a method name. Did you forget an "__invoke" method?
*/
public function testExceptionWhenUsingControllerWithoutAnInvokeMethod()
{
$container = $this->getMockBuilder(Container::class)->getMock();
$container->expects($this->once())
->method('has')
->with('app.my_controller')
->will($this->returnValue(true))
;
$container->expects($this->once())
->method('get')
->with('app.my_controller')
->will($this->returnValue(new ImpossibleConstructController('toto', 'controller')))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', 'app.my_controller');
$resolver->getController($request);
}
/**
* @dataProvider getUndefinedControllers
*/
public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null)
{
// All this logic needs to be duplicated, since calling parent::testGetControllerOnNonUndefinedFunction will override the expected excetion and not use the regex
$resolver = $this->createControllerResolver();
if (method_exists($this, 'expectException')) {
$this->expectException($exceptionName);
$this->expectExceptionMessageRegExp($exceptionMessage);
} else {
$this->setExpectedExceptionRegExp($exceptionName, $exceptionMessage);
}
$request = Request::create('/');
$request->attributes->set('_controller', $controller);
$resolver->getController($request);
}
public function getUndefinedControllers()
{
return array(
array('foo', \LogicException::class, '/Controller not found: service "foo" does not exist\./'),
array('oof::bar', \InvalidArgumentException::class, '/Class "oof" does not exist\./'),
array('stdClass', \LogicException::class, '/Controller not found: service "stdClass" does not exist\./'),
array(
'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar',
\InvalidArgumentException::class,
'/.?[cC]ontroller(.*?) for URI "\/" is not callable\.( Expected method(.*) Available methods)?/',
),
$tests = parent::getUndefinedControllers();
$tests[0] = array('foo', \InvalidArgumentException::class, 'Controller "foo" does neither exist as service nor as class');
$tests[1] = array('oof::bar', \InvalidArgumentException::class, 'Controller "oof" does neither exist as service nor as class');
$tests[2] = array(array('oof', 'bar'), \InvalidArgumentException::class, 'Controller "oof" does neither exist as service nor as class');
$tests[] = array(
array(ControllerTestService::class, 'action'),
\InvalidArgumentException::class,
'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?',
);
$tests[] = array(
ControllerTestService::class.'::action',
\InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?',
);
$tests[] = array(
InvokableControllerService::class,
\InvalidArgumentException::class,
'Controller "Symfony\Component\HttpKernel\Tests\Controller\InvokableControllerService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?',
);
return $tests;
}
protected function createControllerResolver(LoggerInterface $logger = null, ContainerInterface $container = null)
protected function createControllerResolver(LoggerInterface $logger = null, ContainerInterface $container = null): ControllerResolverInterface
{
if (!$container) {
$container = $this->createMockContainer();
@ -268,7 +219,7 @@ class ContainerControllerResolverTest extends ControllerResolverTest
}
}
class InvokableController
class InvokableControllerService
{
public function __construct($bar) // mandatory argument to prevent automatic instantiation
{
@ -279,16 +230,9 @@ class InvokableController
}
}
abstract class NonInstantiableController
class ControllerTestService
{
public static function action()
{
}
}
class ImpossibleConstructController
{
public function __construct($toto, $controller)
public function __construct($foo)
{
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
class ControllerResolverTest extends TestCase
{
@ -41,51 +42,55 @@ class ControllerResolverTest extends TestCase
public function testGetControllerWithObjectAndInvokeMethod()
{
$resolver = $this->createControllerResolver();
$object = new InvokableController();
$request = Request::create('/');
$request->attributes->set('_controller', $this);
$request->attributes->set('_controller', $object);
$controller = $resolver->getController($request);
$this->assertSame($this, $controller);
$this->assertSame($object, $controller);
}
public function testGetControllerWithObjectAndMethod()
{
$resolver = $this->createControllerResolver();
$object = new ControllerTest();
$request = Request::create('/');
$request->attributes->set('_controller', array($this, 'controllerMethod1'));
$request->attributes->set('_controller', array($object, 'publicAction'));
$controller = $resolver->getController($request);
$this->assertSame(array($this, 'controllerMethod1'), $controller);
$this->assertSame(array($object, 'publicAction'), $controller);
}
public function testGetControllerWithClassAndMethod()
public function testGetControllerWithClassAndMethodAsArray()
{
$resolver = $this->createControllerResolver();
$request = Request::create('/');
$request->attributes->set('_controller', array('Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest', 'controllerMethod4'));
$request->attributes->set('_controller', array(ControllerTest::class, 'publicAction'));
$controller = $resolver->getController($request);
$this->assertSame(array('Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest', 'controllerMethod4'), $controller);
$this->assertInstanceOf(ControllerTest::class, $controller[0]);
$this->assertSame('publicAction', $controller[1]);
}
public function testGetControllerWithObjectAndMethodAsString()
public function testGetControllerWithClassAndMethodAsString()
{
$resolver = $this->createControllerResolver();
$request = Request::create('/');
$request->attributes->set('_controller', 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::controllerMethod1');
$request->attributes->set('_controller', ControllerTest::class.'::publicAction');
$controller = $resolver->getController($request);
$this->assertInstanceOf('Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest', $controller[0], '->getController() returns a PHP callable');
$this->assertInstanceOf(ControllerTest::class, $controller[0]);
$this->assertSame('publicAction', $controller[1]);
}
public function testGetControllerWithClassAndInvokeMethod()
public function testGetControllerWithInvokableClass()
{
$resolver = $this->createControllerResolver();
$request = Request::create('/');
$request->attributes->set('_controller', 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest');
$request->attributes->set('_controller', InvokableController::class);
$controller = $resolver->getController($request);
$this->assertInstanceOf('Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest', $controller);
$this->assertInstanceOf(InvokableController::class, $controller);
}
/**
@ -110,10 +115,49 @@ class ControllerResolverTest extends TestCase
$this->assertSame('Symfony\Component\HttpKernel\Tests\Controller\some_controller_function', $controller);
}
public function testGetControllerWithClosure()
{
$resolver = $this->createControllerResolver();
$closure = function () {
return 'test';
};
$request = Request::create('/');
$request->attributes->set('_controller', $closure);
$controller = $resolver->getController($request);
$this->assertInstanceOf(\Closure::class, $controller);
$this->assertSame('test', $controller());
}
/**
* @dataProvider getStaticControllers
*/
public function testGetControllerWithStaticController($staticController, $returnValue)
{
$resolver = $this->createControllerResolver();
$request = Request::create('/');
$request->attributes->set('_controller', $staticController);
$controller = $resolver->getController($request);
$this->assertSame($staticController, $controller);
$this->assertSame($returnValue, $controller());
}
public function getStaticControllers()
{
return array(
array(AbstractController::class.'::staticAction', 'foo'),
array(array(AbstractController::class, 'staticAction'), 'foo'),
array(array(PrivateConstructorController::class, 'staticAction'), 'bar'),
array(array(PrivateConstructorController::class, 'staticAction'), 'bar'),
);
}
/**
* @dataProvider getUndefinedControllers
*/
public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null)
public function testGetControllerWithUndefinedController($controller, $exceptionName = null, $exceptionMessage = null)
{
$resolver = $this->createControllerResolver();
if (method_exists($this, 'expectException')) {
@ -130,34 +174,30 @@ class ControllerResolverTest extends TestCase
public function getUndefinedControllers()
{
$controller = new ControllerTest();
return array(
array(1, 'InvalidArgumentException', 'Unable to find controller "1".'),
array('foo', 'InvalidArgumentException', 'Unable to find controller "foo".'),
array('oof::bar', 'InvalidArgumentException', 'Class "oof" does not exist.'),
array('stdClass', 'InvalidArgumentException', 'Unable to find controller "stdClass".'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'),
array('foo', \Error::class, 'Class \'foo\' not found'),
array('oof::bar', \Error::class, 'Class \'oof\' not found'),
array(array('oof', 'bar'), \Error::class, 'Class \'oof\' not found'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'),
array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'),
array(array($controller, 'staticsAction'), \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'),
array(array($controller, 'privateAction'), \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array(array($controller, 'protectedAction'), \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'),
array(array($controller, 'undefinedAction'), \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'),
array($controller, \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'),
array(array('a' => 'foo', 'b' => 'bar'), \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Invalid array callable, expected array(controller, method).'),
);
}
protected function createControllerResolver(LoggerInterface $logger = null)
protected function createControllerResolver(LoggerInterface $logger = null): ControllerResolverInterface
{
return new ControllerResolver($logger);
}
public function __invoke($foo, $bar = null)
{
}
public function controllerMethod1($foo)
{
}
protected static function controllerMethod4()
{
}
}
function some_controller_function($foo, $foobar)
@ -166,6 +206,15 @@ function some_controller_function($foo, $foobar)
class ControllerTest
{
public function __construct()
{
}
public function __toString()
{
return '';
}
public function publicAction()
{
}
@ -182,3 +231,30 @@ class ControllerTest
{
}
}
class InvokableController
{
public function __invoke($foo, $bar = null)
{
}
}
abstract class AbstractController
{
public static function staticAction()
{
return 'foo';
}
}
class PrivateConstructorController
{
private function __construct()
{
}
public static function staticAction()
{
return 'bar';
}
}

View File

@ -140,10 +140,10 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertEquals(array('foo:fooAction'), array_keys($locator));
$this->assertInstanceof(ServiceClosureArgument::class, $locator['foo:fooAction']);
$this->assertEquals(array('foo::fooAction'), array_keys($locator));
$this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']);
$locator = $container->getDefinition((string) $locator['foo:fooAction']->getValues()[0]);
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
$this->assertSame(ServiceLocator::class, $locator->getClass());
$this->assertFalse($locator->isPublic());
@ -166,7 +166,7 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$pass->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$locator = $container->getDefinition((string) $locator['foo:fooAction']->getValues()[0]);
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
$expected = array('bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, RegisterTestController::class)));
$this->assertEquals($expected, $locator->getArgument(0));
@ -185,7 +185,7 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$pass->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$locator = $container->getDefinition((string) $locator['foo:fooAction']->getValues()[0]);
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
$expected = array('bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, RegisterTestController::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)));
$this->assertEquals($expected, $locator->getArgument(0));
@ -203,7 +203,7 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$pass->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertSame(array('foo:fooAction'), array_keys($locator));
$this->assertSame(array('foo::fooAction'), array_keys($locator));
}
/**
@ -250,7 +250,7 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$pass->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertSame(array('foo:barAction', 'foo:fooAction'), array_keys($locator));
$this->assertSame(array('foo::barAction', 'foo::fooAction'), array_keys($locator));
}
public function testArgumentWithNoTypeHintIsOk()
@ -300,7 +300,7 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$locator = $container->getDefinition((string) $locator['foo:fooAction']->getValues()[0]);
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
$expected = array('bar' => new ServiceClosureArgument(new Reference('foo')));
$this->assertEquals($expected, $locator->getArgument(0));

View File

@ -36,49 +36,30 @@ class RemoveEmptyControllerArgumentLocatorsPassTest extends TestCase
$controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertCount(2, $container->getDefinition((string) $controllers['c1:fooAction']->getValues()[0])->getArgument(0));
$this->assertCount(1, $container->getDefinition((string) $controllers['c2:setTestCase']->getValues()[0])->getArgument(0));
$this->assertCount(1, $container->getDefinition((string) $controllers['c2:fooAction']->getValues()[0])->getArgument(0));
$this->assertCount(2, $container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0));
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0));
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0));
(new ResolveInvalidReferencesPass())->process($container);
$this->assertCount(1, $container->getDefinition((string) $controllers['c2:setTestCase']->getValues()[0])->getArgument(0));
$this->assertSame(array(), $container->getDefinition((string) $controllers['c2:fooAction']->getValues()[0])->getArgument(0));
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0));
$this->assertSame(array(), $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0));
(new RemoveEmptyControllerArgumentLocatorsPass())->process($container);
$controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertSame(array('c1:fooAction'), array_keys($controllers));
$this->assertSame(array('bar'), array_keys($container->getDefinition((string) $controllers['c1:fooAction']->getValues()[0])->getArgument(0)));
$this->assertSame(array('c1::fooAction', 'c1:fooAction'), array_keys($controllers));
$this->assertSame(array('bar'), array_keys($container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0)));
$expectedLog = array(
'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2:fooAction": no corresponding services exist for the referenced types.',
'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2::fooAction": no corresponding services exist for the referenced types.',
'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing method "setTestCase" of service "c2" from controller candidates: the method is called at instantiation, thus cannot be an action.',
);
$this->assertSame($expectedLog, $container->getCompiler()->getLog());
}
public function testSameIdClass()
{
$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.service')->addArgument(array());
$container->register(RegisterTestController::class, RegisterTestController::class)
->addTag('controller.service_arguments')
;
(new RegisterControllerArgumentLocatorsPass())->process($container);
(new RemoveEmptyControllerArgumentLocatorsPass())->process($container);
$expected = array(
RegisterTestController::class.':fooAction',
RegisterTestController::class.'::fooAction',
);
$this->assertEquals($expected, array_keys($container->getDefinition((string) $resolver->getArgument(0))->getArgument(0)));
}
public function testInvoke()
{
$container = new ContainerBuilder();
@ -92,30 +73,10 @@ class RemoveEmptyControllerArgumentLocatorsPassTest extends TestCase
(new RemoveEmptyControllerArgumentLocatorsPass())->process($container);
$this->assertEquals(
array('invokable:__invoke', 'invokable'),
array('invokable::__invoke', 'invokable:__invoke', 'invokable'),
array_keys($container->getDefinition((string) $resolver->getArgument(0))->getArgument(0))
);
}
public function testInvokeSameIdClass()
{
$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.service')->addArgument(array());
$container->register(InvokableRegisterTestController::class, InvokableRegisterTestController::class)
->addTag('controller.service_arguments')
;
(new RegisterControllerArgumentLocatorsPass())->process($container);
(new RemoveEmptyControllerArgumentLocatorsPass())->process($container);
$expected = array(
InvokableRegisterTestController::class.':__invoke',
InvokableRegisterTestController::class.'::__invoke',
InvokableRegisterTestController::class,
);
$this->assertEquals($expected, array_keys($container->getDefinition((string) $resolver->getArgument(0))->getArgument(0)));
}
}
class RemoveTestController1

View File

@ -44,9 +44,14 @@ abstract class ObjectRouteLoader extends Loader
*/
public function load($resource, $type = null)
{
$parts = explode(':', $resource);
if (1 === substr_count($resource, ':')) {
$resource = str_replace(':', '::', $resource);
@trigger_error(sprintf('Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED);
}
$parts = explode('::', $resource);
if (2 != count($parts)) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service_name:methodName"', $resource));
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service::method"', $resource));
}
$serviceString = $parts[0];
@ -58,7 +63,7 @@ abstract class ObjectRouteLoader extends Loader
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', get_class($this), gettype($loaderObject)));
}
if (!method_exists($loaderObject, $method)) {
if (!is_callable(array($loaderObject, $method))) {
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s"', $method, get_class($loaderObject), $resource));
}

View File

@ -18,7 +18,11 @@ use Symfony\Component\Routing\RouteCollection;
class ObjectRouteLoaderTest extends TestCase
{
public function testLoadCallsServiceAndReturnsCollection()
/**
* @group legacy
* @expectedDeprecation Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use my_route_provider_service::loadRoutes instead.
*/
public function testLoadCallsServiceAndReturnsCollectionWithLegacyNotation()
{
$loader = new ObjectRouteLoaderForTest();
@ -40,6 +44,28 @@ class ObjectRouteLoaderTest extends TestCase
$this->assertNotEmpty($actualRoutes->getResources());
}
public function testLoadCallsServiceAndReturnsCollection()
{
$loader = new ObjectRouteLoaderForTest();
// create a basic collection that will be returned
$collection = new RouteCollection();
$collection->add('foo', new Route('/foo'));
$loader->loaderMap = array(
'my_route_provider_service' => new RouteService($collection),
);
$actualRoutes = $loader->load(
'my_route_provider_service::loadRoutes',
'service'
);
$this->assertSame($collection, $actualRoutes);
// the service file should be listed as a resource
$this->assertNotEmpty($actualRoutes->getResources());
}
/**
* @expectedException \InvalidArgumentException
* @dataProvider getBadResourceStrings
@ -54,7 +80,6 @@ class ObjectRouteLoaderTest extends TestCase
{
return array(
array('Foo'),
array('Bar::baz'),
array('Foo:Bar:baz'),
);
}
@ -66,7 +91,7 @@ class ObjectRouteLoaderTest extends TestCase
{
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => 'NOT_AN_OBJECT');
$loader->load('my_service:method');
$loader->load('my_service::method');
}
/**
@ -76,7 +101,7 @@ class ObjectRouteLoaderTest extends TestCase
{
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => new \stdClass());
$loader->load('my_service:method');
$loader->load('my_service::method');
}
/**
@ -93,7 +118,7 @@ class ObjectRouteLoaderTest extends TestCase
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => $service);
$loader->load('my_service:loadRoutes');
$loader->load('my_service::loadRoutes');
}
}