Skip to content

[Routing] Redirection from route configuration #25145

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

Closed

Conversation

sroze
Copy link
Contributor

@sroze sroze commented Nov 24, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #24640
License MIT
Doc PR ø

Following @javiereguiluz's proposal (#24640), this PR allows developers to configure the redirection directly in the routing configuration.

app_entrypoint:
    path: /
    redirect_to: /homepage

@nicolas-grekas
Copy link
Member

PHP-based routing configuration also needs to be patched.

/**
* Common parsing logic between route loaders.
*
* @author Samuel Roze <samuel.roze@gmail.com>
Copy link
Member

Choose a reason for hiding this comment

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

@internal also

@sroze
Copy link
Contributor Author

sroze commented Nov 24, 2017

@nicolas-grekas we did not add the controller feature on the PHP loader, so I guessed this was on purpose. I'm not really sure it's a lot valuable to do so.. what do you think?

$defaults['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction';
$defaults['path'] = $redirect;
} else {
$defaults['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction';
Copy link
Member

@nicolas-grekas nicolas-grekas Nov 24, 2017

Choose a reason for hiding this comment

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

But there is a big glitch here: this is in the Routing namespace, and it references something in FrameworkBundle.
We have to fix that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. For the records, here's @javiereguiluz's answer when @stof's said the same thing. I need to think a bit about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see a few options:

  1. A simple class_exists check on the RedirectController to have a user-friendly exception. This references the FrameworkBundle, but is not a hard coupling.
  2. We use a _redirect attribute, that will be handled by a ControllerResolverInterface implementation within the FrameworkBundle.
  3. We use a _redirect attribute that will be handled by a listener on kernel.request within the HttpKernel component

@nicolas-grekas nicolas-grekas added this to the 4.1 milestone Nov 24, 2017
@sroze
Copy link
Contributor Author

sroze commented Nov 24, 2017

Right, so you mean we should add a redirect method in the RouteTrait?

private function looksLikeUrl(string $urlOrRouteName): bool
{
foreach (array('/', '//', 'http://', 'https://') as $pattern) {
if (substr($urlOrRouteName, 0, strlen($pattern)) == $pattern) {
Copy link
Member

Choose a reason for hiding this comment

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

should use 0 === strpos() instead to be consistent with the codebase (we avoid allocating a new string)

private function redirectToDefaults(string $redirect, bool $permanent = false): array
{
if ($this->looksLikeUrl($redirect)) {
$defaults['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction';
Copy link
Member

Choose a reason for hiding this comment

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

would be better to write it as $defaults = array(..., ...) instead of assigning in an undefined variable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure how that happened - well, I know, refactoring.. - , I'd expect PHP to tell me how bad was that.

@@ -239,6 +241,10 @@ private function parseConfigs(\DOMElement $node, $path)
$defaults['_controller'] = $controller;
}

if ($redirectTo = $node->getAttribute('redirect-to')) {
Copy link
Member

Choose a reason for hiding this comment

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

should forbid to put it at the same time than a controller (check for $defaults['_controller'] being configured already)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 👍

@@ -239,6 +241,10 @@ private function parseConfigs(\DOMElement $node, $path)
$defaults['_controller'] = $controller;
}

if ($redirectTo = $node->getAttribute('redirect-to')) {
$defaults += $this->redirectToDefaults($redirectTo, $node->getAttribute('redirect-permanent'));
Copy link
Member

Choose a reason for hiding this comment

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

not good. The attribute value will always be a string. It must be parsed into an actual boolean (see the values accepted by parseDefaultNode for the <bool> tag)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But.. it's working well... not sure getAttribute actually returns only strings 🤔

@nicolas-grekas
Copy link
Member

As stated in #24640 (comment) and below, I would do it differently: instead of setting a controller, the redirect_to entry could just set a new entry in "defaults", named eg "_redirect_to" instead of "_controller". Then it'd be up to high level layers to act upon it.

@sroze
Copy link
Contributor Author

sroze commented Nov 24, 2017

@nicolas-grekas definitely agree, see my proposed options here.

return $defaults;
}

private function looksLikeUrl(string $urlOrRouteName): bool
Copy link
Member

Choose a reason for hiding this comment

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

I fear any guessing strategy would be fragile. Instead, what about two defaults:

  • _redirect_to_route
  • _redirect_to_uri

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is a valid fear: if the string starts with / or a [scheme]:// then it's an URL seems enough to me. Having two ways of configuring the "target" reduces the DX.

Copy link
Member

Choose a reason for hiding this comment

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

Moreover, we already do that guessing in other parts of the framework. E.g. the login_path option in the always delicate Security component.

@sroze
Copy link
Contributor Author

sroze commented Nov 24, 2017

The real question now is: who is going to handle the _redirect (or two _redirects) attributes? HttpKernel or FrameworkBundle? I'd argue for HttpKernel so it can benefit other libraries only using the kernel. If it's HttpKernel, we need to move the logic from FrameworkBundle's RedirectController to the HttpKernel component.

@nicolas-grekas
Copy link
Member

Adding one more question we need to answer:
what about the "permanent" flag, and the "ignoreAttributes" setting?

@sroze
Copy link
Contributor Author

sroze commented Nov 24, 2017

what about the "permanent" flag, and the "ignoreAttributes" setting?

I'd say redirect_permanent and redirect_ignore_attributes configurations (and their XML equivalents with -s)

@sroze sroze force-pushed the redirect-from-route-configuration branch 2 times, most recently from 0fa3e8e to 484121e Compare November 25, 2017 13:36
@sroze
Copy link
Contributor Author

sroze commented Nov 25, 2017

@stof @nicolas-grekas I've updated the PR to use _redirect_to and _redirect_permanent attributes populated by the loaders in Routing. A new listener in FrameworkBundle adds the _controller on kernel.request when it sees the _redirect_* attributes in the Request.

@nicolas-grekas
Copy link
Member

@sroze what would be the new way of doing this configuration in yaml? Can we do without a listener (yet another one for this looks like overhead to me...)

@sroze
Copy link
Contributor Author

sroze commented Nov 26, 2017

what would be the new way of doing this configuration in yaml?

The one in the PR description :)

Can we do without a listener (yet another one for this looks like overhead to me...)

Well... we could. Do you want me to merge with the existing one or doing something else? 🤔

@sroze sroze force-pushed the redirect-from-route-configuration branch 2 times, most recently from 62e7de8 to 5a35a7f Compare December 3, 2017 11:45
@sroze sroze force-pushed the redirect-from-route-configuration branch from 8edd537 to 50f9243 Compare January 4, 2018 16:36
@sroze
Copy link
Contributor Author

sroze commented Jan 5, 2018

AppVeyor's failure is not related. It's a network issue. Can somebody restart it?

@sroze
Copy link
Contributor Author

sroze commented Jan 15, 2018

ping @Tobion :)

@sroze sroze force-pushed the redirect-from-route-configuration branch from 50f9243 to 3266887 Compare January 26, 2018 13:52
@nicolas-grekas
Copy link
Member

Now that I know the router a bit better, I'd suggest leveraging RedirectableUrlMatcherInterface::redirect() instead. This also means I'd suggest to remove the permanent/non-permanent flag. Let's make this very simple, and make it a core feature on the Routing component only, since is has everything needed to deal with that standalone.

@sroze
Copy link
Contributor Author

sroze commented Feb 11, 2018

I'd suggest leveraging RedirectableUrlMatcherInterface::redirect() instead.

Can you guide me through what you have in mind a little bit more? Maybe one way would be to roughly explain how you'd change this pull-request?

@nicolas-grekas
Copy link
Member

when a _redirect entry is found in the matched route, a redirectable matcher could internally
return $this->redirect($ret['_redirect'], $ret['_route']);

@javiereguiluz
Copy link
Member

I've been thinking about this proposal ... and I propose to make a minor change. Instead of using redirect_to for redirecting both URLs and routes, we could be more explicit and define redirect_to and redirect_to_route (this is also consistent with other Symfony parts, where "redirect" is for URLs and routes have a special redirect method). In practice:

# redirect_to for absolute or relative URLs
blog_show:
    path: /blog/{slug}
    redirect_to: /some-relative-url
    permanent: true

# redirect_to_route without route arguments
blog_show:
    path: /blog/{slug}
    redirect_to_route: 'some-other-route'
    # this also works, to be consistent with the next case:
    # redirect_to_route: ['some-other-route']

# redirect_to_route with route arguments
blog_show:
    path: /blog/{slug}
    redirect_to_route: ['some-other-route', { arg1: 'value1', arg2: 'value2' }]

@javiereguiluz javiereguiluz added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Mar 12, 2018
@javiereguiluz
Copy link
Member

@sroze I know you are very busy and you also have a much more important PR related to the new Message component ... but it'd be great if you could finish this feature before the Symfony 4.1 "feature freeze" starts in 12 days. Thanks!

@sroze
Copy link
Contributor Author

sroze commented Mar 19, 2018

@javiereguiluz yep, agree with you, it would be nice. I'll update the PR this week :)

@sroze
Copy link
Contributor Author

sroze commented Mar 20, 2018

I've explored the option of moving this logic directly within the router, especially within the PhpMatcherDumper and it worked for the redirect idea. It did not work at all for the redirect_to_route idea because it would require a UrlGeneratorInterface while the dumper matcher is... just a matcher. One way to tackle this would be to have a sort of mini-generator while dumping the matcher but this 1) would duplicate some logic and 2) won't support things like route parameters. This is therefore not a satisfying approach.

The problem with the current implementation is that is it specific to a few details about the routing and is sort of between the Routing component and the FrameworkBundle bundle. Redirection configuration (while allowed by the Routing component) wouldn't work without the FrameworkBundle, which is not what we'd like.

While discussing all of that with @nicolas-grekas, we jumped back to actually... what we are trying to solve: simplifying the YAML configuration. It might be that the only issue, for now, is the Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction FQCN that we always have to copy/paste. Nico's idea, which might make sense is that instead of redirect-specific configuration, we simply add controller aliases. Therefore, the redirection configuration could look like that:

redirect_to_admin:
    path:     /
    defaults:
        _controller: _redirect_path
        path: /admin
        permanent: true

What do you all think?

@javiereguiluz
Copy link
Member

javiereguiluz commented Mar 20, 2018

I haven't looked at the internals ... but from the outside this looks "just" a syntactic sugar that should be "easy" to implement. So, the end-user writes this in YAML:

blog_show:
    path:        /blog/{slug}
    redirect_to: /some-relative-url
    permanent:   true

And Symfony, when parsing that YAML file, transforms it to this transparently:

blog_show:
    path:     /blog/{slug}
    defaults:
        _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
        path: /some-relative-url
        permanent: true

So, this feature "just" needs to look for a redirect_to option and then, create the defaults._controller and defaults.path options automatically ... and let Symfony take care of the rest. No more changes would be needed because the verbose config already works: this feature "just" transforms concise config to verbose config internally. Would that work?

@sroze
Copy link
Contributor Author

sroze commented Mar 20, 2018

@javiereguiluz the real problem is that:

The problem with the current implementation is that is it specific to a few details about the routing and is sort of between the Routing component and the FrameworkBundle bundle. Redirection configuration (while allowed by the Routing component) wouldn't work without the FrameworkBundle, which is not what we'd like.

@javiereguiluz
Copy link
Member

javiereguiluz commented Mar 20, 2018

@sroze this is how I imagine the implementation of this feature (the code doesn't work, I just invented it to show what I'm asking for):

public function processRoutes($yamlContent)
{
    $routesConfig = Yaml::parse($yamlContent);
    foreach ($routesConfig as $routeConfig) {
+        if (isset($routeConfig['redirect_to'])) {
+           $routeConfig['_defaults']['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction';
+           $routeConfig['_defaults']['path'] = $routeConfig['redirect_to'];
+           unset($routeConfig['redirect_to']);
+       }

        // ...
    }

    return $routeCollection;
}

@sroze
Copy link
Contributor Author

sroze commented Mar 20, 2018

Yes, I agree with you. But it would mean the Routing component depends on the FrameworkBundle (at least, the way it is now).

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 20, 2018

I'm now convinced the aliasing feature is all that is needed at least as a first step. This should be a feature of RouterListener in HttpKernel IMHO (could also be in Routing matchers, but I feel better with RouterListener). So down this path, we would just need a RouterListener::setControllerAliases(array $aliases) and done.

Javier has another goal, which is to move defaults out of the "defaults" nesting level.
This goal is unrelated to redirection to me. Or, if it's for redirections only, then I'm very unsure, as this would just create a new specific case that ppl will need to learn.

All-in-all, the controller aliasing looks like the right next step to me.

@javiereguiluz
Copy link
Member

javiereguiluz commented Mar 22, 2018

To better compare the alternatives, here is a summary:

Current situation

blog_show:
    path: /blog/{slug}
    defaults:
        _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
        path: /some-relative-url
        permanent: true

Proposal 1

blog_show:
    path: /blog/{slug}
    redirect_to: /some-relative-url
    permanent: true

Proposal 2

blog_show:
    path: /blog/{slug}
    defaults:
        _controller: _redirect_path
        path: /some-relative-url
        permanent: true

I think it's OK to create a redirect_to option because we recently moved the defaults._controller option to just controller and people loved it.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 22, 2018

I'm 👎 for adding redirect specific root-options because it creates a very specific case people will have to learn, whereas the proposed alternative (2) just leverages existing and new generic knowledge. The redirect-specific entries would also prevent using new or exotic redirection options, because these are specific to the actual redirector implementation.

@sroze
Copy link
Contributor Author

sroze commented Mar 22, 2018

The only counter-argument to @nicolas-grekas' PoV I can see is that a redirection is not a "very specific case" of routing.

{
$requestAttributes = $event->getRequest()->attributes;

if (!$requestAttributes->has('_controller') && $redirectTo = $requestAttributes->get('_redirect_to')) {
Copy link
Member

Choose a reason for hiding this comment

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

I would use early returns instead:

if ($requestAttributes->has('_controller')) {
    return;
}

if (!$redirectTo = $requestAttributes->get('_redirect_to')) {
    return;
}

);
}

public function onKernelRequest(GetResponseEvent $event)
Copy link
Member

Choose a reason for hiding this comment

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

public function onKernelRequest(GetResponseEvent $event): void

*/
class ResolveRedirectControllerSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
Copy link
Member

Choose a reason for hiding this comment

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

public static function getSubscribedEvents(): array

@fabpot
Copy link
Member

fabpot commented Mar 23, 2018

From a user PoV, I like the redirect_to option from Javier. Looks neat to me. That does not work well as it would not allow for specific HTTP status code, ...

@sroze
Copy link
Contributor Author

sroze commented Apr 4, 2018

Let's close this one, as this direction is a dead end :)

@sroze sroze closed this Apr 4, 2018
@sroze sroze deleted the redirect-from-route-configuration branch April 4, 2018 16:01
nicolas-grekas added a commit that referenced this pull request Feb 9, 2020
…le template and redirect controllers (HeahDude)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[FrameworkBundle][Routing] added Configurators to handle template and redirect controllers

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | let's see
| Fixed tickets | partially #24640, #25145
| License       | MIT
| Doc PR        | symfony/symfony-docs#11120

While working on symfony/symfony-docs#11085, I felt bad about the long notations required for simple [redirects](https://symfony.com/doc/current/routing/redirect_in_config.html) and [templates rendering](https://symfony.com/doc/current/templating/render_without_controller.html) template actions, but I love and use those features since always. Then I gave it a try yesterday night and now I realised I missed #24640 and that #25145 has been closed x).

So here we go, here's my WIP. WDYT of this implementation? ping @javiereguiluz?

I'm going to open the PR in the docs so we can discuss the DX changes there too, and keep focus on the code here.

Cheers!

EDIT
----
This PR now only update PHP-DSL configurators.

______________

TODO:

- [x] gather reviews
- ~[x] fix xml schema~
- [x] add some tests
- ~[ ] handle xsd auto discovery~
- [x] rebase on top of #30507
- [x] ~add shortcuts for #30514~

Commits
-------

de74794 [FrameworkBundle][Routing] added Configurators to handle template and redirect controllers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Routing ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Needs Review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants