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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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\Bundle\FrameworkBundle\EventListener;

use Symfony\Bundle\FrameworkBundle\Controller\RedirectController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Resolve the controller for requests containing the `_redirect_to` attributes.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
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

{
return array(
KernelEvents::REQUEST => array('onKernelRequest', 20),
);
}

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

{
$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;
}

if ($this->looksLikeUrl($redirectTo)) {
$requestAttributes->set('_controller', array(RedirectController::class, 'urlRedirectAction'));
$requestAttributes->set('path', $redirectTo);
} else {
$requestAttributes->set('_controller', array(RedirectController::class, 'redirectAction'));
$requestAttributes->set('route', $redirectTo);
}

if (!$requestAttributes->has('permanent')) {
$requestAttributes->set('permanent', $requestAttributes->get('_redirect_permanent', false));
}
}
}

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

Choose a reason for hiding this comment

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

private static? 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's the point? 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

perf? dunno sorry :P just a habit of mine. Like how it shows utility / non class-API.

{
foreach (array('/', 'http://', 'https://') as $pattern) {
Copy link
Contributor

Choose a reason for hiding this comment

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

+ '?', '#'?

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 what you mean here?

Copy link
Contributor

Choose a reason for hiding this comment

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

?query and #frag looks like URL, thus should return true no?

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 wouldn't bother with that tbf

Copy link
Member

Choose a reason for hiding this comment

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

Do we really want to implement such fuzzy code? I mean when configuring a redirection through the routing config before, you always had to distinguish URLs and paths yourself. Why don't we do that here too (e.g. use redirect_to_route and redirect_to_url attributes)?

if (0 === strpos($urlOrRouteName, $pattern)) {
return true;
}
}

return false;
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,9 @@
<argument type="service" id="controller_name_converter" />
<tag name="kernel.event_subscriber" />
</service>

<service id="resolve_redirect_controller_name_subscriber" class="Symfony\Bundle\FrameworkBundle\EventListener\ResolveRedirectControllerSubscriber">
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?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\Bundle\FrameworkBundle\Tests\EventListener;

use Symfony\Bundle\FrameworkBundle\Controller\RedirectController;
use Symfony\Bundle\FrameworkBundle\EventListener\ResolveRedirectControllerSubscriber;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class ResolveRedirectControllerSubscriberTest extends TestCase
{
/**
* @dataProvider provideRedirectionExamples
*/
public function testSetControllerForRedirectToRoute(Request $request, array $expectedAttributes)
{
$httpKernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$subscriber = new ResolveRedirectControllerSubscriber();
$subscriber->onKernelRequest(new GetResponseEvent($httpKernel, $request, HttpKernelInterface::MASTER_REQUEST));

foreach ($expectedAttributes as $name => $value) {
$this->assertEquals($value, $request->attributes->get($name));
}
}

public function provideRedirectionExamples()
{
// No redirection
yield array($this->requestWithAttributes(array(
'_controller' => 'AppBundle:Starting:format',
)), array(
'_controller' => 'AppBundle:Starting:format',
));

// Controller win over redirection
yield array($this->requestWithAttributes(array(
'_controller' => 'AppBundle:Starting:format',
'_redirect_to' => 'https://google.com',
)), array(
'_controller' => 'AppBundle:Starting:format',
));

// Redirection to URL
yield array($this->requestWithAttributes(array(
'_redirect_to' => 'https://google.com',
)), array(
'_controller' => array(RedirectController::class, 'urlRedirectAction'),
'path' => 'https://google.com',
));

// Redirection to route
yield array($this->requestWithAttributes(array(
'_redirect_to' => 'route',
)), array(
'_controller' => array(RedirectController::class, 'redirectAction'),
'route' => 'route',
));

// Permanent redirection to route
yield array($this->requestWithAttributes(array(
'_redirect_to' => 'route',
'_redirect_permanent' => true,
)), array(
'_controller' => array(RedirectController::class, 'redirectAction'),
'route' => 'route',
'permanent' => true,
));
}

private function requestWithAttributes(array $attributes): Request
{
$request = new Request();

foreach ($attributes as $name => $value) {
$request->attributes->set($name, $value);
}

return $request;
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Component/Routing/Loader/XmlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,15 @@ 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 👍

if (isset($defaults['_controller'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both a controller and a redirection.', $path));
}

$defaults['_redirect_to'] = $redirectTo;
$defaults['_redirect_permanent'] = (bool) $node->getAttribute('redirect-permanent');
}

return array($defaults, $requirements, $options, $condition);
}

Expand Down
11 changes: 10 additions & 1 deletion src/Symfony/Component/Routing/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
class YamlFileLoader extends FileLoader
{
private static $availableKeys = array(
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix',
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'redirect_to', 'redirect_permanent',
);
private $yamlParser;

Expand Down Expand Up @@ -120,6 +120,15 @@ protected function parseRoute(RouteCollection $collection, $name, array $config,
$defaults['_controller'] = $config['controller'];
}

if (isset($config['redirect_to'])) {
if (isset($defaults['_controller'])) {
throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both a controller and a redirection.', $path));
}

$defaults['_redirect_to'] = $config['redirect_to'];
$defaults['_redirect_permanent'] = $config['redirect_permanent'] ?? false;
}

$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);

$collection->add($name, $route);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<xsd:attribute name="schemes" type="xsd:string" />
<xsd:attribute name="methods" type="xsd:string" />
<xsd:attribute name="controller" type="xsd:string" />
<xsd:attribute name="redirect-to" type="xsd:string" />
<xsd:attribute name="redirect-permanent" type="xsd:boolean" />
</xsd:complexType>

<xsd:complexType name="import">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="app_symfony" path="/symfony" redirect-to="https://symfony.com" controller="App\Controller\Symfony" />
</routes>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
app_symfony:
path: /symfony
controller: App\Controller\Symfony
redirect_to: https://symfony.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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" />
<route id="app_temporary_redirect" path="/home" redirect-to="app_homepage" />
<route id="app_permanent_redirect" path="/permanent-home" redirect-to="/" redirect-permanent="true" />
</routes>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
app_homepage:
path: /
controller: AppBundle:Homepage:show

app_temporary_redirect:
path: /home
redirect_to: app_homepage

app_permanent_redirect:
path: /permanent-home
redirect_to: /
redirect_permanent: true
27 changes: 27 additions & 0 deletions src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,4 +372,31 @@ public function testImportRouteWithNamePrefix()
$this->assertNotNull($routeCollection->get('api_app_blog'));
$this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath());
}

public function testRedirections()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to')));
$routeCollection = $loader->load('routing.xml');

$route = $routeCollection->get('app_homepage');
$this->assertSame('AppBundle:Homepage:show', $route->getDefault('_controller'));

$route = $routeCollection->get('app_temporary_redirect');
$this->assertSame('app_homepage', $route->getDefault('_redirect_to'));
$this->assertFalse($route->getDefault('_redirect_permanent'));

$route = $routeCollection->get('app_permanent_redirect');
$this->assertSame('/', $route->getDefault('_redirect_to'));
$this->assertTrue($route->getDefault('_redirect_permanent'));
}

/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both a controller and a redirection\./
*/
public function testRedirectionCannotBeUsedWithController()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to')));
$loader->load('redirect_with_controller.xml');
}
}
27 changes: 27 additions & 0 deletions src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,31 @@ public function testImportRouteWithNamePrefix()
$this->assertNotNull($routeCollection->get('api_app_blog'));
$this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath());
}

public function testRedirections()
{
$loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to')));
$routeCollection = $loader->load('routing.yml');

$route = $routeCollection->get('app_homepage');
$this->assertSame('AppBundle:Homepage:show', $route->getDefault('_controller'));

$route = $routeCollection->get('app_temporary_redirect');
$this->assertSame('app_homepage', $route->getDefault('_redirect_to'));
$this->assertFalse($route->getDefault('_redirect_permanent'));

$route = $routeCollection->get('app_permanent_redirect');
$this->assertSame('/', $route->getDefault('_redirect_to'));
$this->assertTrue($route->getDefault('_redirect_permanent'));
}

/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both a controller and a redirection\./
*/
public function testRedirectionCannotBeUsedWithController()
{
$loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to')));
$loader->load('redirect_with_controller.yml');
}
}