diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php
index 425f607f84a07..3f0ef31bbca2c 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php
@@ -35,12 +35,14 @@ class RouterDebugCommand extends Command
{
protected static $defaultName = 'debug:router';
private $router;
+ private $parser;
- public function __construct(RouterInterface $router)
+ public function __construct(RouterInterface $router, ControllerNameParser $parser)
{
parent::__construct();
$this->router = $router;
+ $this->parser = $parser;
}
/**
@@ -109,9 +111,8 @@ protected function execute(InputInterface $input, OutputInterface $output)
private function convertController(Route $route)
{
if ($route->hasDefault('_controller')) {
- $nameParser = new ControllerNameParser($this->getApplication()->getKernel());
try {
- $route->setDefault('_controller', $nameParser->build($route->getDefault('_controller')));
+ $route->setDefault('_controller', $this->parser->build($route->getDefault('_controller')));
} catch (\InvalidArgumentException $e) {
}
}
@@ -133,9 +134,8 @@ private function extractCallable(Route $route)
}
}
- $nameParser = new ControllerNameParser($this->getApplication()->getKernel());
try {
- $shortNotation = $nameParser->build($controller);
+ $shortNotation = $this->parser->build($controller);
$route->setDefault('_controller', $shortNotation);
return $controller;
diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php
index 21b13f91e8cc7..2d80cdc4634ce 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php
@@ -11,7 +11,11 @@
namespace Symfony\Bundle\FrameworkBundle\Controller;
+use Symfony\Component\HttpKernel\Controller\ActionReference;
+use Symfony\Component\HttpKernel\Controller\ControllerLayoutInterface;
+use Symfony\Component\HttpKernel\Controller\Layout\GenericControllerLayout;
use Symfony\Component\HttpKernel\KernelInterface;
+use Symfony\Component\HttpKernel\Util\AlternativeBundleNameProvider;
/**
* ControllerNameParser converts controller from the short notation a:b:c
@@ -24,9 +28,12 @@ class ControllerNameParser
{
protected $kernel;
- public function __construct(KernelInterface $kernel)
+ private $layout;
+
+ public function __construct(KernelInterface $kernel, ControllerLayoutInterface $layout = null)
{
$this->kernel = $kernel;
+ $this->layout = $layout ?: new GenericControllerLayout($this->kernel);
}
/**
@@ -60,19 +67,16 @@ public function parse($controller)
$originalController
);
- if ($alternative = $this->findAlternative($bundleName)) {
+ $provider = new AlternativeBundleNameProvider($this->kernel);
+
+ if ($alternative = $provider->findAlternative($bundleName)) {
$message .= sprintf(' Did you mean "%s:%s:%s"?', $alternative, $controller, $action);
}
throw new \InvalidArgumentException($message, 0, $e);
}
- $try = $bundle->getNamespace().'\\Controller\\'.$controller.'Controller';
- if (class_exists($try)) {
- return $try.'::'.$action.'Action';
- }
-
- throw new \InvalidArgumentException(sprintf('The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. Create this class or check the spelling of the class and its namespace.', $bundleName, $controller, $action, $try));
+ return $this->layout->build(new ActionReference($bundle, $controller, $action));
}
/**
@@ -86,48 +90,8 @@ public function parse($controller)
*/
public function build($controller)
{
- if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) {
- throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "class::method" string.', $controller));
- }
-
- $className = $match[1];
- $controllerName = $match[2];
- $actionName = $match[3];
- foreach ($this->kernel->getBundles() as $name => $bundle) {
- if (0 !== strpos($className, $bundle->getNamespace())) {
- continue;
- }
-
- return sprintf('%s:%s:%s', $name, $controllerName, $actionName);
- }
-
- throw new \InvalidArgumentException(sprintf('Unable to find a bundle that defines controller "%s".', $controller));
- }
-
- /**
- * Attempts to find a bundle that is *similar* to the given bundle name.
- */
- private function findAlternative(string $nonExistentBundleName): ?string
- {
- $bundleNames = array_map(function ($b) {
- return $b->getName();
- }, $this->kernel->getBundles());
-
- $alternative = null;
- $shortest = null;
- foreach ($bundleNames as $bundleName) {
- // if there's a partial match, return it immediately
- if (false !== strpos($bundleName, $nonExistentBundleName)) {
- return $bundleName;
- }
-
- $lev = levenshtein($nonExistentBundleName, $bundleName);
- if ($lev <= strlen($nonExistentBundleName) / 3 && (null === $alternative || $lev < $shortest)) {
- $alternative = $bundleName;
- $shortest = $lev;
- }
- }
+ $reference = $this->layout->parse($controller);
- return $alternative;
+ return sprintf('%s:%s:%s', $reference->getBundle()->getName(), $reference->getController(), $reference->getAction());
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
index 34f47a0599b31..5c6e23a9c4c08 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
@@ -66,6 +66,7 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
index 0622c4196c104..e6f5717324c0d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
@@ -7,9 +7,14 @@
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php
index 54fb8db8c6bee..8d0f44e574353 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand;
use Symfony\Component\HttpKernel\KernelInterface;
@@ -52,8 +53,9 @@ public function testDebugInvalidRoute()
*/
private function createCommandTester()
{
- $application = new Application($this->getKernel());
- $application->add(new RouterDebugCommand($this->getRouter()));
+ $kernel = $this->getKernel();
+ $application = new Application($kernel);
+ $application->add(new RouterDebugCommand($this->getRouter(), new ControllerNameParser($kernel)));
return new CommandTester($application->find('debug:router'));
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php
index 7baa874355df2..a1fb480f06370 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand;
use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand;
@@ -46,9 +47,10 @@ public function testWithNotMatchPath()
*/
private function createCommandTester()
{
- $application = new Application($this->getKernel());
+ $kernel = $this->getKernel();
+ $application = new Application($kernel);
$application->add(new RouterMatchCommand($this->getRouter()));
- $application->add(new RouterDebugCommand($this->getRouter()));
+ $application->add(new RouterDebugCommand($this->getRouter(), new ControllerNameParser($kernel)));
return new CommandTester($application->find('router:match'));
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php
index 0dfed269ec20e..f60eae7cf0cbf 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php
@@ -145,6 +145,7 @@ private function createParser()
{
$bundles = array(
'SensioCmsFooBundle' => $this->getBundle('TestBundle\Sensio\Cms\FooBundle', 'SensioCmsFooBundle'),
+ 'FoooooBundle' => $this->getBundle('TestBundle\FooBundle', 'FoooooBundle'),
'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'),
);
@@ -161,11 +162,6 @@ private function createParser()
}))
;
- $bundles = array(
- 'SensioCmsFooBundle' => $this->getBundle('TestBundle\Sensio\Cms\FooBundle', 'SensioCmsFooBundle'),
- 'FoooooBundle' => $this->getBundle('TestBundle\FooBundle', 'FoooooBundle'),
- 'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'),
- );
$kernel
->expects($this->any())
->method('getBundles')
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index dd086e2c3efca..e57df71f2eb8c 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -23,7 +23,7 @@
"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": "~3.4|~4.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "~3.4|~4.0",
"symfony/finder": "~3.4|~4.0",
diff --git a/src/Symfony/Component/HttpKernel/Controller/ActionReference.php b/src/Symfony/Component/HttpKernel/Controller/ActionReference.php
new file mode 100644
index 0000000000000..89a1b4a489221
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Controller/ActionReference.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Controller;
+
+use Symfony\Component\HttpKernel\Bundle\BundleInterface;
+
+/**
+ * Class for holding bundle + controller name + action name.
+ *
+ * @author Pavel Batanov
+ */
+final class ActionReference
+{
+ /** @var BundleInterface */
+ private $bundle;
+ /** @var string */
+ private $controller;
+ /** @var string */
+ private $action;
+
+ public function __construct(BundleInterface $bundle, string $controller, string $action)
+ {
+ $this->bundle = $bundle;
+ $this->controller = $controller;
+ $this->action = $action;
+ }
+
+ public function getBundle(): BundleInterface
+ {
+ return $this->bundle;
+ }
+
+ public function getController(): string
+ {
+ return $this->controller;
+ }
+
+ public function getAction(): string
+ {
+ return $this->action;
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerLayoutInterface.php b/src/Symfony/Component/HttpKernel/Controller/ControllerLayoutInterface.php
new file mode 100644
index 0000000000000..16e7f65d4ede4
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Controller/ControllerLayoutInterface.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Controller;
+
+use Symfony\Component\HttpKernel\Exception\ControllerLayoutException;
+
+/**
+ * Responsible for build FQCN::action from bundle + controller name + action name
+ * and parse FQCN::action into bundle + controller name + action name.
+ *
+ * @author Pavel Batanov
+ */
+interface ControllerLayoutInterface
+{
+ /**
+ * Decompose controller string into bundle, controller and action.
+ *
+ * @param string $controller
+ *
+ * @return ActionReference
+ *
+ * @throws ControllerLayoutException
+ */
+ public function parse(string $controller): ActionReference;
+
+ /**
+ * Builds a controller string for given bundle, controller, and action.
+ *
+ * @param ActionReference $action
+ *
+ * @return string
+ *
+ * @throws ControllerLayoutException
+ */
+ public function build(ActionReference $action): string;
+}
diff --git a/src/Symfony/Component/HttpKernel/Controller/Layout/GenericControllerLayout.php b/src/Symfony/Component/HttpKernel/Controller/Layout/GenericControllerLayout.php
new file mode 100644
index 0000000000000..f641a7dee5c65
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Controller/Layout/GenericControllerLayout.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Controller\Layout;
+
+use Symfony\Component\HttpKernel\Controller\ActionReference;
+use Symfony\Component\HttpKernel\Controller\ControllerLayoutInterface;
+use Symfony\Component\HttpKernel\Exception\ControllerLayoutException;
+use Symfony\Component\HttpKernel\KernelInterface;
+
+/**
+ * Class for generic *Bundle/Controller/*Controller::*Action layout.
+ *
+ * @author Fabien Potencier
+ * @author Pavel Batanov
+ */
+final class GenericControllerLayout implements ControllerLayoutInterface
+{
+ /**
+ * @var KernelInterface
+ */
+ private $kernel;
+
+ /**
+ * GenericControllerLayout constructor.
+ *
+ * @param KernelInterface $kernel
+ */
+ public function __construct(KernelInterface $kernel)
+ {
+ $this->kernel = $kernel;
+ }
+
+ public function parse(string $controller): ActionReference
+ {
+ if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) {
+ throw new \InvalidArgumentException(
+ sprintf('The "%s" controller is not a valid "class::method" string.', $controller)
+ );
+ }
+
+ [,$className, $controllerName, $actionName] = $match;
+ foreach ($this->kernel->getBundles() as $name => $bundle) {
+ if (0 !== strpos($className, $bundle->getNamespace())) {
+ continue;
+ }
+
+ return new ActionReference($this->kernel->getBundle($name), $controllerName, $actionName);
+ }
+
+ throw ControllerLayoutException::unknownBundleForController($controller);
+ }
+
+ /** {@inheritdoc} */
+ public function build(ActionReference $reference): string
+ {
+ $try = $reference->getBundle()->getNamespace().'\\Controller\\'.$reference->getController().'Controller';
+
+ if (!class_exists($try)) {
+ throw ControllerLayoutException::unknownControllerClass($reference, $try);
+ }
+
+ return $try.'::'.$reference->getAction().'Action';
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Exception/ControllerLayoutException.php b/src/Symfony/Component/HttpKernel/Exception/ControllerLayoutException.php
new file mode 100644
index 0000000000000..25aa507ce57ff
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Exception/ControllerLayoutException.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Exception;
+
+use Symfony\Component\HttpKernel\Controller\ActionReference;
+
+/**
+ * @author Pavel Batanov
+ */
+class ControllerLayoutException extends \InvalidArgumentException
+{
+ public static function unknownControllerClass(ActionReference $reference, string $try): self
+ {
+ throw new static(
+ sprintf(
+ 'The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. Create this class or check the spelling of the class and its namespace.',
+ $reference->getBundle()->getName(),
+ $reference->getController(),
+ $reference->getAction(),
+ $try
+ )
+ );
+ }
+
+ public static function unknownBundleForController(string $controller): self
+ {
+ return new static(sprintf('Unable to find a bundle that defines controller "%s".', $controller));
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Util/AlternativeBundleNameProvider.php b/src/Symfony/Component/HttpKernel/Util/AlternativeBundleNameProvider.php
new file mode 100644
index 0000000000000..de357bdad7f29
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Util/AlternativeBundleNameProvider.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Util;
+
+use Symfony\Component\HttpKernel\Bundle\BundleInterface;
+use Symfony\Component\HttpKernel\KernelInterface;
+
+final class AlternativeBundleNameProvider
+{
+ /** @var KernelInterface */
+ private $kernel;
+
+ /**
+ * AlternativeBundleNameProvider constructor.
+ *
+ * @param KernelInterface $kernel
+ */
+ public function __construct(KernelInterface $kernel)
+ {
+ $this->kernel = $kernel;
+ }
+
+ /**
+ * Attempts to find a bundle that is *similar* to the given bundle name.
+ */
+ public function findAlternative(string $nonExistentBundleName): ?string
+ {
+ $bundleNames = array_map(function (BundleInterface $b) {
+ return $b->getName();
+ }, $this->kernel->getBundles());
+
+ $alternative = null;
+ $shortest = null;
+ foreach ($bundleNames as $bundleName) {
+ // if there's a partial match, return it immediately
+ if (false !== strpos($bundleName, $nonExistentBundleName)) {
+ return $bundleName;
+ }
+
+ $lev = levenshtein($nonExistentBundleName, $bundleName);
+ if ($lev <= strlen($nonExistentBundleName) / 3 && (null === $alternative || $lev < $shortest)) {
+ $alternative = $bundleName;
+ $shortest = $lev;
+ }
+ }
+
+ return $alternative;
+ }
+}