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:
commit
6651087a98
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
),
|
||||
));
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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'));
|
||||
?>
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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 }
|
||||
|
@ -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}$" }
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user