-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[HttpKernel] [DX] Configurable controller layout #25422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there only 1 alternative? I can imagine there could be multiple alternatives. Assuming 1 alternative is the status quo There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just moved this responsibility (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php#L107-L132) out to the separate class since it could be reused in many bundle guessing snippets It's logic is about finding most similar bundle name, so this definetely only 1. We can return prioritized list here leaving formatting to the end-user code |
||
} | ||
|
||
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()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,9 +7,14 @@ | |
<services> | ||
<defaults public="false" /> | ||
|
||
<service id="controller_layout.generic" class="Symfony\Component\HttpKernel\Controller\Layout\GenericControllerLayout"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could probably be the FQCN as ID, though I'm not sure if this is default for the Symfony Core. If not, I'd like to see an alias for autowiring purposes (perhaps on the interface as well if this is the default). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are no FQCN identifiers in this file, neither did I. I think this is a step for further improvements outside of the scope of this PR |
||
<argument type="service" id="kernel" /> | ||
</service> | ||
|
||
<service id="controller_name_converter" class="Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser"> | ||
<tag name="monolog.logger" channel="request" /> | ||
<argument type="service" id="kernel" /> | ||
<argument type="service" id="controller_layout.generic" /> | ||
</service> | ||
|
||
<service id="controller_resolver" class="Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver"> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpKernel\Controller; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleInterface; | ||
|
||
/** | ||
* Class for holding bundle + controller name + action name. | ||
* | ||
* @author Pavel Batanov <pavel@batanov.me> | ||
*/ | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpKernel\Controller; | ||
|
||
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 <pavel@batanov.me> | ||
*/ | ||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As there is (currently) no configuration for this, how would someone change this to resolve it differently? Do you have examples of why it should be solved differently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General idea is to reuse the class itself for other layouts. So we can use default generic layout for controllers and custom layouts with same logic for something else.
Am not sure that this class should live here, but not in http-kernel component (to be used standalone), but moving a class is a tricky thing, so I haven't suggest it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be more concrete we have secondary routing for RPC controllers allowing to specify
a:b:c
notation which points toRPC
namespace. Currently we have to copy-paste this class to override layout and keep the resolving process