Skip to content

[Routing] Fix i18n routing when the url contains the locale #35101

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
Jan 4, 2020
Merged
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
Expand Up @@ -40,7 +40,6 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT
if (null !== $locale) {
do {
if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
unset($parameters['_locale']);
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 needed anymore because it is handled by the new code.

Copy link
Member

Choose a reason for hiding this comment

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

wouldn't it be OK to do $parameters['_locale'] = $locale here, and remove the other changes?

Copy link
Contributor Author

@fancyweb fancyweb Dec 26, 2019

Choose a reason for hiding this comment

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

No because it's not enough to handle the case where the generator locale is "fr" + you just ask for the "foo.en" route. In this case the locale remains "fr".

$name .= '.'.$locale;
break;
}
Expand All @@ -53,6 +52,14 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT

list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = $this->compiledRoutes[$name];

if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This if = we are in i18n routing. Checking isset($defaults['_locale']) is a safety.

if (!\in_array('_locale', $variables, true)) {
unset($parameters['_locale']);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We always unset the _locale parameter if the url does not have the _locale variable because the good i18n route has already been found and we don't want the parameter.

} elseif (!isset($parameters['_locale'])) {
$parameters['_locale'] = $defaults['_locale'];
}
}

return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT
if (null !== $locale && null !== $name) {
do {
if ((self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
unset($parameters['_locale']);
$name .= '.'.$locale;
break;
}
Expand All @@ -133,6 +132,14 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT

list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = self::$declaredRoutes[$name];

if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
if (!\in_array('_locale', $variables, true)) {
unset($parameters['_locale']);
} elseif (!isset($parameters['_locale'])) {
$parameters['_locale'] = $defaults['_locale'];
}
}

return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
EOF;
Expand Down
14 changes: 12 additions & 2 deletions src/Symfony/Component/Routing/Generator/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT
if (null !== $locale) {
do {
if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
unset($parameters['_locale']);
break;
}
} while (false !== $locale = strstr($locale, '_', true));
Expand All @@ -147,7 +146,18 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT
// the Route has a cache of its own and is not recompiled as long as it does not get modified
$compiledRoute = $route->compile();

return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes());
$defaults = $route->getDefaults();
$variables = $compiledRoute->getVariables();

if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
if (!\in_array('_locale', $variables, true)) {
unset($parameters['_locale']);
} elseif (!isset($parameters['_locale'])) {
$parameters['_locale'] = $defaults['_locale'];
}
}

return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ public function testDumpWithRouteNotFoundLocalizedRoutes()

public function testDumpWithFallbackLocaleLocalizedRoutes()
{
$this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test'));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When a route is i18n, it always has a default _locale. The test must reflect that (same for the other ones).

$this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_locale', 'nl')->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'test'));

$code = $this->generatorDumper->dump();
file_put_contents($this->testTmpFilepath, $code);
Expand Down Expand Up @@ -231,4 +231,29 @@ public function testDumpWithSchemeRequirement()
$this->assertEquals('https://localhost/app.php/testing', $absoluteUrl);
$this->assertEquals('/app.php/testing', $relativeUrl);
}

public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl()
{
$this->routeCollection->add('foo.en', (new Route('/{_locale}/foo'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'foo'));
$this->routeCollection->add('foo.fr', (new Route('/{_locale}/foo'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'foo'));
$this->routeCollection->add('fun.en', (new Route('/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'fun'));
$this->routeCollection->add('fun.fr', (new Route('/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'fun'));

file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());

$requestContext = new RequestContext();
$requestContext->setParameter('_locale', 'fr');

$compiledUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, $requestContext, null, null);

$this->assertSame('/fr/foo', $compiledUrlGenerator->generate('foo'));
$this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo.en'));
$this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo', ['_locale' => 'en']));
$this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo.fr', ['_locale' => 'en']));

$this->assertSame('/amusant', $compiledUrlGenerator->generate('fun'));
$this->assertSame('/fun', $compiledUrlGenerator->generate('fun.en'));
$this->assertSame('/fun', $compiledUrlGenerator->generate('fun', ['_locale' => 'en']));
$this->assertSame('/amusant', $compiledUrlGenerator->generate('fun.fr', ['_locale' => 'en']));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ public function testDumpWithRouteNotFoundLocalizedRoutes()

public function testDumpWithFallbackLocaleLocalizedRoutes()
{
$this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_locale', 'nl')->setDefault('_canonical_route', 'test'));
$this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'test'));

$code = $this->generatorDumper->dump([
'class' => 'FallbackLocaleLocalizedProjectUrlGenerator',
Expand Down Expand Up @@ -250,4 +250,32 @@ public function testDumpWithSchemeRequirement()
$this->assertEquals('https://localhost/app.php/testing', $absoluteUrl);
$this->assertEquals('/app.php/testing', $relativeUrl);
}

public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl()
{
$this->routeCollection->add('foo.en', (new Route('/{_locale}/foo'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'foo'));
$this->routeCollection->add('foo.fr', (new Route('/{_locale}/foo'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'foo'));
$this->routeCollection->add('fun.en', (new Route('/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'fun'));
$this->routeCollection->add('fun.fr', (new Route('/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'fun'));

file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump([
'class' => 'PreserveTheGoodLocaleInTheUrlGenerator',
]));
include $this->testTmpFilepath;

$requestContext = new RequestContext();
$requestContext->setParameter('_locale', 'fr');

$phpGenerator = new \PreserveTheGoodLocaleInTheUrlGenerator($requestContext);

$this->assertSame('/fr/foo', $phpGenerator->generate('foo'));
$this->assertSame('/en/foo', $phpGenerator->generate('foo.en'));
$this->assertSame('/en/foo', $phpGenerator->generate('foo', ['_locale' => 'en']));
$this->assertSame('/en/foo', $phpGenerator->generate('foo.fr', ['_locale' => 'en']));

$this->assertSame('/amusant', $phpGenerator->generate('fun'));
$this->assertSame('/fun', $phpGenerator->generate('fun.en'));
$this->assertSame('/fun', $phpGenerator->generate('fun', ['_locale' => 'en']));
$this->assertSame('/amusant', $phpGenerator->generate('fun.fr', ['_locale' => 'en']));
}
}
23 changes: 23 additions & 0 deletions src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,29 @@ public function testGenerateWithOverriddenParameterLocaleFromRequestContext()
);
}

public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl()
{
$routeCollection = new RouteCollection();

$routeCollection->add('foo.en', (new Route('/{_locale}/foo'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'foo'));
$routeCollection->add('foo.fr', (new Route('/{_locale}/foo'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'foo'));
$routeCollection->add('fun.en', (new Route('/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'fun'));
$routeCollection->add('fun.fr', (new Route('/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'fun'));

$urlGenerator = $this->getGenerator($routeCollection);
$urlGenerator->getContext()->setParameter('_locale', 'fr');

$this->assertSame('/app.php/fr/foo', $urlGenerator->generate('foo'));
$this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo.en'));
$this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo', ['_locale' => 'en']));
$this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo.fr', ['_locale' => 'en']));

$this->assertSame('/app.php/amusant', $urlGenerator->generate('fun'));
$this->assertSame('/app.php/fun', $urlGenerator->generate('fun.en'));
$this->assertSame('/app.php/fun', $urlGenerator->generate('fun', ['_locale' => 'en']));
$this->assertSame('/app.php/amusant', $urlGenerator->generate('fun.fr', ['_locale' => 'en']));
}

public function testGenerateWithoutRoutes()
{
$this->expectException('Symfony\Component\Routing\Exception\RouteNotFoundException');
Expand Down