Skip to content

[Routing] Add PHP fluent DSL for configuring routes #24180

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
Sep 20, 2017

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Sep 13, 2017

Q A
Branch? 3.4
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets -
License MIT
Doc PR -

If we add a PHP DSL for DI config (#23834), we must have a similar one for routing. Here it is. See fixtures.

So, you always start with a RoutingConfigurator $routes, which allows you to:

$routes->add($name, $path); // adds a route
$routes->import($resource, $type = null, $ignoreErrors = false); // imports routes
$routes->collection($name = ''); // starts a collection, all *names* might be prefixed

then

  • for "import" and "collection", you can ->prefix($path);
  • for "add" and "collection", you can fluently "add" several times;
  • for "collection" you add sub"->collection()";
  • and on all, you can configure the route(s) with:
->defaults(array $defaults)
->requirements(array $requirements)
->options(array $options)
->condition($condition)
->host($pattern)
->schemes(array $schemes)
->methods(array $methods)
->controller($controller)

@javiereguiluz
Copy link
Member

I like this proposal! Thanks for contributing it.

My only super tiny concern is about a variable name. I propose to rename RoutingConfigurator $route to RoutingConfigurator $routes I know it's just a single letter difference, but to me it changes its meaning completely:

// add something to the given route
$route->add(...);

// add a route to the given collection of routes
$routes->add(...);

A more real comparison using your fixture:

// BEFORE
return function (RoutingConfigurator $route) {
    $route
        ->add('foo', '/foo')
            ->condition('abc')
            ->options(array('utf8' => true))
        ->add('buz', 'zub')
            ->controller('foo:act');

    $route->import('php_dsl_sub.php')
        ->prefix('/sub')
        ->requirements(array('id' => '\d+'));

    $route->add('ouf', '/ouf')
        ->schemes(array('https'))
        ->methods(array('GET'))
        ->defaults(array('id' => 0));
};

// AFTER
return function (RoutingConfigurator $routes) {
    $routes
        ->add('foo', '/foo')
            ->condition('abc')
            ->options(array('utf8' => true))
        ->add('buz', 'zub')
            ->controller('foo:act');

    $routes->import('php_dsl_sub.php')
        ->prefix('/sub')
        ->requirements(array('id' => '\d+'));

    $routes->add('ouf', '/ouf')
        ->schemes(array('https'))
        ->methods(array('GET'))
        ->defaults(array('id' => 0));
};

@nicolas-grekas
Copy link
Member Author

@javiereguiluz fixtures updated.

@sstok
Copy link
Contributor

sstok commented Sep 14, 2017

The collection will make it so much easier to add routes with a common prefix without having to store those routes in a separate file! I love this 👏

return function (RoutingConfigurator $routes) {
    $news = $routes->collection('news_')
        ->prefix('news/')
        ->requirements(['id' => '\d+'])
    ;

    // No need to repeat the name prefix, the requirements are applied here but
    // are still part of the same file.
    $news('show', '{id}')->controller('News:show');
    $news('edit', '{id}/edit')->controller('News:edit');
    $news('delete', '{id}/delete')->controller('News:delete');
};

Copy link
Member

@javiereguiluz javiereguiluz left a comment

Choose a reason for hiding this comment

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

I like it! Thanks for this contribution.

}

/**
* Sets the prefix to add the path of all child routes.
Copy link
Member

Choose a reason for hiding this comment

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

This description looks like it's missing something. What about?

Sets the prefix added to the path of all child routes.

}

/**
* Sets the prefix to add the path of all child routes.
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here.

*
* @return $this
*/
final public function schemes(array $schemes)
Copy link
Member

Choose a reason for hiding this comment

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

Would you consider using variadics here and in methods() method too?

@@ -37,7 +38,21 @@ public function load($file, $type = null)
$path = $this->locator->locate($file);
$this->setCurrentDir(dirname($path));

$collection = self::includeFile($path, $this);
// the closure forbids access the private scope in the included file
Copy link
Member

Choose a reason for hiding this comment

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

forbids access the private scope -> forbids accessing the private scope or forbids access to the private scope ?

@nicolas-grekas
Copy link
Member Author

@javiereguiluz thanks, comments addressed.

Would you consider using variadics here and in methods() method too?

I wouldn't, for the same "least surprise" reason. If someone wants to push stronger for variadics, I suggest doing so in a dedicated PR, after this one (and the DI one) have been merged.

@ogizanagi ogizanagi self-requested a review September 20, 2017 07:30
Copy link
Contributor

@robfrawley robfrawley left a comment

Choose a reason for hiding this comment

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

This looks great! 👍

class CollectionConfigurator
{
use Traits\AddTrait;
use Traits\RouteTrait;
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't ->defaults, ->requirements and so on apply on $this->collection in this class ?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's the case, but not on the collection yes, because it works reverse: calling on the collection means changing routes declared before. Here, we want first to configure the route prototype ($this->route in this class), then use this prototype as a template for new routes added.
The goal is to make this work as expected:

$collection->defaults(['foo' => 'bar'])->add(...);

but I'm not sure doing $collection->add(...); then $collection->defaults(['foo' => 'bar']); and having it applied to previously declared routes in the collection is something we want to have.

@nicolas-grekas nicolas-grekas merged commit f433c9a into symfony:3.4 Sep 20, 2017
nicolas-grekas added a commit that referenced this pull request Sep 20, 2017
…icolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[Routing] Add PHP fluent DSL for configuring routes

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

If we add a PHP DSL for DI config (#23834), we must have a similar one for routing. Here it is. See fixtures.

So, you always start with a `RoutingConfigurator $routes`, which allows you to:
```php
$routes->add($name, $path); // adds a route
$routes->import($resource, $type = null, $ignoreErrors = false); // imports routes
$routes->collection($name = ''); // starts a collection, all *names* might be prefixed
```

then
- for "import" and "collection", you can `->prefix($path)`;
- for "add" and "collection", you can fluently "add" several times;
- for "collection" you add sub"`->collection()`";
- and on all, you can configure the route(s) with:
```php
->defaults(array $defaults)
->requirements(array $requirements)
->options(array $options)
->condition($condition)
->host($pattern)
->schemes(array $schemes)
->methods(array $methods)
->controller($controller)
```

Commits
-------

f433c9a [Routing] Add PHP fluent DSL for configuring routes
@nicolas-grekas nicolas-grekas deleted the routing-dsl branch September 20, 2017 13:06
*
* @return $this
*/
final public function controller($controller)
Copy link
Contributor

@ajessu ajessu Sep 27, 2017

Choose a reason for hiding this comment

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

I had an idea looking at the examples on the main blog post about this feature (which looks great btw): http://symfony.com/blog/new-in-symfony-php-based-configuration-for-services-and-routes

Is it possible, or does it make sense to change the api of this controller method (or even adding a new helper method), to enable use Controller::class, without having to concatenate the results?

If I'm not mistaken, all other class methods accept a FQN class name as its parameter, so you can pass a string, or use the ::class (which I find less error prone since it's IDE auto completed).

This one however, since it's following the config that already exist would end in being something like:

->add('homepage', '/')
    ->controller('App\Controller\DefaultController::index')

or

->add('homepage', '/')
    ->controller(DefaultController::class . '::index')

In the later scenario, would changing this method's parameters or adding a new method that would accept the method of the controller separately help, as far as DX goes?

as an example:

->add('homepage', '/')
    ->controller(DefaultController::class, 'index')

I understand that wouldn't match the full string config for a controller, in relation to other configurations, so that might be a con, and concatenating honestly doesn't look that bad either, just wanted to raise the case, since it's different from all others, in case it makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ajessu That's indeed a very good idea 👍 of course this action parameter should be optional as more advanced developers use a single action per controller (called Action controllers).

Copy link
Member Author

Choose a reason for hiding this comment

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

that's already possible: just use the array-callable PHP syntax:
->controller([DefaultController::class, 'index'])

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @nicolas-grekas. That seems perfect. Wasn't aware that was a possibility here.

@efermi
Copy link

efermi commented Sep 28, 2017

    $routes
        ->add('foo', '/foo')
            ->condition('abc')
            ->options(array('utf8' => true))
        ->add('buz', 'zub')
            ->controller('foo:act');

This is too fluent, code formatters will break the indent-based semantics and the interface looks confusing at the first glance.

@nicolas-grekas
Copy link
Member Author

If you don't want to be too fluent, you can just do:

    $routes->add('foo', '/foo')
            ->condition('abc')
            ->options(array('utf8' => true));
    $routes->add('buz', 'zub')
            ->controller('foo:act');

It's up to you.

@efermi
Copy link

efermi commented Sep 28, 2017

Of course, I understand that, just wanted to share my first impressions; it is a welcome change after all.

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.

9 participants