Skip to content

Add support for "controller" keyword for configuring routes controllers #23227

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

Merged
merged 1 commit into from
Aug 16, 2017
Merged

Add support for "controller" keyword for configuring routes controllers #23227

merged 1 commit into from
Aug 16, 2017

Conversation

voronkovich
Copy link
Contributor

Q A
Branch? 3.4
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
License MIT

This PR adds a syntax sugar for configuring routes controllers in more user-friendly way by using a controller keyword.

# Current syntax
# It looks strange and exposes Symfony's internals
blog_show:
    path:     /blog/{slug}
    defaults: { _controller: AppBundle:Blog:show }

# Suggested syntax
blog_show:
    path:       /blog/{slug}
    controller: AppBundle:Blog:show
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <!-- Current syntax -->
    <route id="blog_list" path="/blog">
        <default key="_controller">AppBundle:Blog:list</default>
    </route>

    <!-- Suggested syntax -->
    <route id="blog_list" path="/blog" controller="AppBundle:Blog:list" />
</routes>

@voronkovich voronkovich changed the title Use "controller" keyword for configuring routes controllers Add support for "controller" keyword for configuring routes controllers Jun 19, 2017

app_blog:
path: /blog
controller: AppBundle:Homepage:show
Copy link
Contributor

@ogizanagi ogizanagi Jun 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for this not to throw, but let the defaults override it?
IMHO, it'd be better to throw an exception when both controller attribute and _controller default are set, in order to avoid some potential WTF moments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ogizanagi, I think you're right. I've changed the code to throw the exception when both controller attribute and _controller default are set.

*
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
*/
class ControllerAwareXmlFileLoader extends XmlFileLoader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using a separate file loader instead of adding the feature in the existing one ? AFAICT, adding the new feature is fully backward compatible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked myself the same. But I came to the conclusion it's legit, as the controller parameter is "tied" to the HttpKernel usage of the Routing component.
But then, I wonder if this rather belong to the HttpKernel component or the FrameworkBundle instead?

Copy link
Contributor

@ogizanagi ogizanagi Jun 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, RouteCollectionBuilder::add() also sets the controller and is in the component itself...
Eventually, it's probably not worth it to create separate loaders for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof, The routing component doesn’t know about "controllers", "views", "models" and all things like that.

The Routing component maps an HTTP request to a set of configuration variables.

The controller keyword is valueless when you use the routing component as a standalone library. So I thinks it's better to extend the existing loaders than change them.

@@ -27,7 +27,7 @@
*/
class YamlFileLoader extends FileLoader
{
private static $availableKeys = array(
protected static $availableKeys = array(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces inheritance extension points, which then have to be maintained forever. And inheritance extension points are the worse to maintain for BC. So -1

@javiereguiluz
Copy link
Member

I'm not sure about this proposal. It's better for DX ... but it introduces another way to configure something that you can already configure it in several ways.

@stof
Copy link
Member

stof commented Jun 20, 2017

Thus, it might then confuse people about why there is _controller in attributes of the Request object

@voronkovich
Copy link
Contributor Author

@stof, I think it's not a problem. Most users prefer to configure routes using annotations and don't know about the Request's _controller parameter.

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Jul 3, 2017
@fabpot
Copy link
Member

fabpot commented Jul 3, 2017

I like the idea. Back then, I didn't know if we would have more than _controller. 5 years later, I think it's a good time to actually recognize that using _controller is not so nice and move it to a first class citizen.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Jul 3, 2017

Same to me, DX wise, it's way easier to use.
About the implem, I think we should do that in the existing loaders.
I get the argument that the router doesn't know about what a "controller" is, but a loader just maps some input to an output. Adding this case doesn't mean the routing component becomes controller-aware all of a sudden. This will ease the patch and its maintenance.

@szymach
Copy link

szymach commented Jul 3, 2017

Out of curiosity, is there a way to not define a controller and have the route work? Aside from it being just a placeholder, like for example the logout route. If there is no other way to use routes that to map them to controllers, it should just by a first-class citizen.

@stof
Copy link
Member

stof commented Jul 3, 2017

I totally agree about using the existing loader. It also means that other projects uisng the component without FrameworkBundle (Silex, Drupal, etc...) get the same experience.

@voronkovich
Copy link
Contributor Author

@stof, @nicolas-grekas, Okay, maybe you're right. I've made changes to use the existing loaders instead of creating the new ones.

* A controller's key name (e.g. '_controller').
*
* @var string
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't document private properties. This can be removed

private $controllerKey;

/**
* Constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can eb removed

/**
* Constructor.
*
* @param FileLocatorInterface $locator A FileLocatorInterface instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed as well

throw new \InvalidArgumentException(sprintf(
'The routing file "%s" must not specify both the "controller" attribute and the defaults key "%s" for %s.',
$path, $this->controllerKey, $name
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be on one line

*
* @var mixed
*/
private $controllerKey;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phpdocs to be removed

private $controllerKey;

/**
* Constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be removed

/**
* Constructor.
*
* @param FileLocatorInterface $locator A FileLocatorInterface instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be removed

throw new \InvalidArgumentException(sprintf(
'The routing file "%s" must not specify both the "controller" key and the defaults key "%s" for "%s".',
$path, $this->controllerKey, $name
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be on one line

@voronkovich
Copy link
Contributor Author

@fabpot, I've fixed all the CS issues you pointed out.

/**
* @param string $controllerKey A controller's key name
*/
public function __construct(FileLocatorInterface $locator, $controllerKey = 'controller')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not _controller as default?
why do we need this to be configurable?

@@ -26,11 +26,13 @@
<service id="routing.loader.xml" class="Symfony\Component\Routing\Loader\XmlFileLoader">
<tag name="routing.loader" />
<argument type="service" id="file_locator" />
<argument>_controller</argument>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if _controller is going to be the default in the loaders constructors, then this should be removed (same below)

@voronkovich
Copy link
Contributor Author

@nicolas-grekas, Fixed. I've removed the controllerKey property. Now the _controller key is hardcoded in the loaders.

@nicolas-grekas
Copy link
Member

W don't merge PRs with merge commit, please rebase instead.

@voronkovich
Copy link
Contributor Author

@nicolas-grekas, sorry, I used the GitHub's web interface to resolve conflict and didn't know that it creates merge commit. I've removed that commit and rebased the PR. Thanks!

@@ -13,8 +13,8 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep original ordering (that's our policy: alpha order for new items, "as is" for existing ones)

@@ -6,6 +6,7 @@ CHANGELOG

* Added support for prioritized routing loaders.
* Add matched and default parameters to redirect responses
* Added support for a `controller` keyword for configuring routes controllers in YAML and XML configurations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

routes controllers (route with no "s")

@@ -11,11 +11,11 @@

namespace Symfony\Component\Routing\Loader;

use Symfony\Component\Routing\RouteCollection;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep original ordering

@@ -229,6 +229,16 @@ private function parseConfigs(\DOMElement $node, $path)
}
}

if ($controller = $node->getAttribute('controller')) {
if (isset($defaults['_controller'])) {
$name = $node->hasAttribute('id') ? sprintf('"%s"', $node->getAttribute('id')) : sprintf('the %s tag', $node->tagName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing quotes around the 2nd %s

if (isset($defaults['_controller'])) {
$name = $node->hasAttribute('id') ? sprintf('"%s"', $node->getAttribute('id')) : sprintf('the %s tag', $node->tagName);

throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for %s.', $path, $name));
Copy link
Member

@nicolas-grekas nicolas-grekas Jul 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great to formulate the message so that $path is last

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Route "%s" must must not specify both the "controller" attribute and the defaults key "_controller" in file "%s"' ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sstok, yes, but all other messages have a format with a file path at the beginning. I think we should keep the consistency.

@@ -11,12 +11,12 @@

namespace Symfony\Component\Routing\Loader;

use Symfony\Component\Routing\RouteCollection;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original ordering

@@ -116,6 +116,10 @@ protected function parseRoute(RouteCollection $collection, $name, array $config,
$methods = isset($config['methods']) ? $config['methods'] : array();
$condition = isset($config['condition']) ? $config['condition'] : null;

if ($controller = isset($config['controller']) ? $config['controller'] : null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!empty($config['controller'])) {
    $defaults['_controller'] = $config['controller'];
}

@@ -141,6 +145,10 @@ protected function parseImport(RouteCollection $collection, array $config, $path
$schemes = isset($config['schemes']) ? $config['schemes'] : null;
$methods = isset($config['methods']) ? $config['methods'] : null;

if ($controller = isset($config['controller']) ? $config['controller'] : null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="app_homepage" path="/" controller="AppBundle:Homepage:show" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also have another imported route that does not specify the controller attribute.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what about a case where an imported route makes use of the _controller default?

* @expectedException \InvalidArgumentException
* @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both the "controller" attribute and the defaults key "_controller" for the "import" tag/
*/
public function testImportWithOverridedController()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overridden

* @expectedException \InvalidArgumentException
* @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both the "controller" key and the defaults key "_controller" for "_static"/
*/
public function testImportWithOverridedController()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overridden

public function testOverrideControllerInDefaults()
{
$loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller')));
$routeCollection = $loader->load('override_defaults.yml');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$routeCollection is not used

public function testImportWithOverridedController()
{
$loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller')));
$routeCollection = $loader->load('import_override_defaults.yml');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$routeCollection is not used

public function testImportWithOverridedController()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller')));
$routeCollection = $loader->load('import_override_defaults.xml');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$routeCollection is not used

public function testOverrideControllerInDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/controller')));
$routeCollection = $loader->load('override_defaults.xml');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$routeCollection is not used

@@ -141,6 +145,10 @@ protected function parseImport(RouteCollection $collection, array $config, $path
$schemes = isset($config['schemes']) ? $config['schemes'] : null;
$methods = isset($config['methods']) ? $config['methods'] : null;

if (!empty($config['controller'])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not isset() instead?

@@ -116,6 +116,10 @@ protected function parseRoute(RouteCollection $collection, $name, array $config,
$methods = isset($config['methods']) ? $config['methods'] : array();
$condition = isset($config['condition']) ? $config['condition'] : null;

if (!empty($config['controller'])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not isset() instead?

Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed my downvote :) this is solved rather elegant actually 👍

@@ -204,5 +212,8 @@ protected function validate($config, $name, $path)
$name, $path
));
}
if (isset($config['controller']) && isset($config['defaults']['_controller'])) {
Copy link
Contributor

@ro0NL ro0NL Jul 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be written as isset($a, $b)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly you can't. See #22684.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, i've forgot about that. 👍

@voronkovich
Copy link
Contributor Author

@xabbuh, thanks for the code review! I've fixed all issues you pointed out and added the missing test cases.


$route = $routeCollection->get('app_homepage');

$this->assertSame('FrameworkBundle:Template:template', $route->getDefault('_controller'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here should now have an assertion for the other import route to have the _controller attribute to be set to the value configured in the import.xml file (same for the YamlFileLoaderTest)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xabbuh, I've added the missing assertions.

Copy link
Member

@xabbuh xabbuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a case where the importing file makes use of the new controller option while the imported file makes use of the _controller attribute (and vice versa)? Should that be a supported use case?

@voronkovich
Copy link
Contributor Author

@xabbuh, I think you're right! I've added the test case you suggested.

@nicolas-grekas
Copy link
Member

Thank you @voronkovich.

@nicolas-grekas nicolas-grekas merged commit 06bbee8 into symfony:3.4 Aug 16, 2017
nicolas-grekas added a commit that referenced this pull request Aug 16, 2017
…outes controllers (voronkovich)

This PR was merged into the 3.4 branch.

Discussion
----------

Add support for "controller" keyword for configuring routes controllers

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| License       | MIT

This PR adds a syntax sugar for configuring routes controllers in more user-friendly way by using a `controller` keyword.

```yaml
# Current syntax
# It looks strange and exposes Symfony's internals
blog_show:
    path:     /blog/{slug}
    defaults: { _controller: AppBundle:Blog:show }

# Suggested syntax
blog_show:
    path:       /blog/{slug}
    controller: AppBundle:Blog:show
```
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <!-- Current syntax -->
    <route id="blog_list" path="/blog">
        <default key="_controller">AppBundle:Blog:list</default>
    </route>

    <!-- Suggested syntax -->
    <route id="blog_list" path="/blog" controller="AppBundle:Blog:list" />
</routes>
```

Commits
-------

06bbee8 [Routing] Use "controller" keyword for configuring routes controllers
@nicolas-grekas
Copy link
Member

@voronkovich can you please open a doc PR/issue?

@voronkovich
Copy link
Contributor Author

@nicolas-grekas, the issue is already created (thank you, @HeahDude!): symfony/symfony-docs#8240

@Pierstoval
Copy link
Contributor

@szymach commented on 3 Jul
Out of curiosity, is there a way to not define a controller and have the route work? Aside from it being just a placeholder, like for example the logout route. If there is no other way to use routes that to map them to controllers, it should just by a first-class citizen.

FYI, the defaults: { _controller: ... } is not mandatory. It's just "default" values. _controller is only used by the kernel to know what callable will be executed to render the response. If you create routes with no controller and handle the responses in kernel listeners instead, it'll be ok.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.