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; + } +}