Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b2fafc6

Browse files
feature #26143 [Routing] Implement i18n routing (frankdejonge, nicolas-grekas)
This PR was merged into the 4.1-dev branch. Discussion ---------- [Routing] Implement i18n routing | Q | A | ------------- | --- | Branch? | master | Bug fix? |no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | N/A | License | MIT This PR introduces support for I18N routing into core. This is a port from a bundle I've made recently, now merged into the default implementation. While it's ok to have this as a bundle, it was suggested by @nicolas-grekas to create a PR for this so it can be included into the core. ## New usages ### YAML ```yaml contact: controller: ContactController::formAction path: en: /send-us-an-email nl: /stuur-ons-een-email ``` Will be effectively the same as declaring: ```yaml contact.en: controller: ContactController::formAction path: /send-us-an-email defaults: _locale: en contact.nl: controller: ContactController::formAction path: /stuur-ons-een-email defaults: _locale: nl ``` ### Annotation usage: ```php <?php use Symfony\Component\Routing\Annotation\Route; class ContactController { /** * @route({"en": "/send-us-an-email", "nl": "/stuur-ons-een-email"}, name="contact") */ public function formAction() { } } /** * @route("/contact") */ class PrefixedContactController { /** * @route({"en": "/send-us-an-email", "nl": "/stuur-ons-een-email"}, name="contact") */ public function formAction() { } } ``` ### Route generation ```php <?php /** @var UrlGeneratorInterface $urlGenerator */ $urlWithCurrentLocale = $urlGenerator->generate('contact'); $urlWithSpecifiedLocale = $urlGenerator->generate('contact', ['_locale' => 'nl']); ``` Route generation is based on your request locale. When not available it falls back on a configured default. This way of route generation means you have a "route locale switcher" out of the box, but generate the current route with another locale for most cases. ## Advantages Having i18n routes defined like this has some advantages: * Less error prone. * No need to keep `requirements` or `defaults` in sync with other definitions. * No need to `{_locale}` in the path (bad for route matching performance). * Better developer experience. ### Next steps I've ported all the things the bundle supported, before moving on I'd like to discuss this first in order not to waste our collective time. This initial PR should give a clear enough picture to see what/how/why this is done. If and when accepted I/we can move forward to implement the XML loader and @nicolas-grekas mentioned there should be a `Configurator` implemented for this as well. He opted to help with this (for which I'm very thankful). - [x] Yaml Loader - [x] Annotation Loader - [x] XML Loader - [x] PHP Loader? - [ ] Documentation Commits ------- 4ae66dc [Routing] Handle "_canonical_route" e32c414 [Routing] Implement i18n routing
2 parents ceffe76 + 4ae66dc commit b2fafc6

File tree

56 files changed

+1141
-241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1141
-241
lines changed

src/Symfony/Component/Routing/Annotation/Route.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class Route
2323
{
2424
private $path;
25+
private $locales = array();
2526
private $name;
2627
private $requirements = array();
2728
private $options = array();
@@ -38,11 +39,20 @@ class Route
3839
*/
3940
public function __construct(array $data)
4041
{
42+
if (isset($data['locales'])) {
43+
throw new \BadMethodCallException(sprintf('Unknown property "locales" on annotation "%s".', get_class($this)));
44+
}
45+
4146
if (isset($data['value'])) {
42-
$data['path'] = $data['value'];
47+
$data[is_array($data['value']) ? 'locales' : 'path'] = $data['value'];
4348
unset($data['value']);
4449
}
4550

51+
if (isset($data['path']) && is_array($data['path'])) {
52+
$data['locales'] = $data['path'];
53+
unset($data['path']);
54+
}
55+
4656
foreach ($data as $key => $value) {
4757
$method = 'set'.str_replace('_', '', $key);
4858
if (!method_exists($this, $method)) {
@@ -62,6 +72,16 @@ public function getPath()
6272
return $this->path;
6373
}
6474

75+
public function setLocales(array $locales)
76+
{
77+
$this->locales = $locales;
78+
}
79+
80+
public function getLocales(): array
81+
{
82+
return $this->locales;
83+
}
84+
6585
public function setHost($pattern)
6686
{
6787
$this->host = $pattern;

src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ public function dump(array $options = array())
5454
class {$options['class']} extends {$options['base_class']}
5555
{
5656
private static \$declaredRoutes;
57+
private \$defaultLocale;
5758
58-
public function __construct(RequestContext \$context, LoggerInterface \$logger = null)
59+
public function __construct(RequestContext \$context, LoggerInterface \$logger = null, string \$defaultLocale = null)
5960
{
6061
\$this->context = \$context;
6162
\$this->logger = \$logger;
63+
\$this->defaultLocale = \$defaultLocale;
6264
if (null === self::\$declaredRoutes) {
6365
self::\$declaredRoutes = {$this->generateDeclaredRoutes()};
6466
}
@@ -107,7 +109,14 @@ private function generateGenerateMethod()
107109
return <<<'EOF'
108110
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
109111
{
110-
if (!isset(self::$declaredRoutes[$name])) {
112+
$locale = $parameters['_locale']
113+
?? $this->context->getParameter('_locale')
114+
?: $this->defaultLocale;
115+
116+
if (null !== $locale && (self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) {
117+
unset($parameters['_locale']);
118+
$name .= '.'.$locale;
119+
} elseif (!isset(self::$declaredRoutes[$name])) {
111120
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
112121
}
113122

src/Symfony/Component/Routing/Generator/UrlGenerator.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
3737

3838
protected $logger;
3939

40+
private $defaultLocale;
41+
4042
/**
4143
* This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
4244
*
@@ -65,11 +67,12 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
6567
'%7C' => '|',
6668
);
6769

68-
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null)
70+
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null)
6971
{
7072
$this->routes = $routes;
7173
$this->context = $context;
7274
$this->logger = $logger;
75+
$this->defaultLocale = $defaultLocale;
7376
}
7477

7578
/**
@@ -109,7 +112,13 @@ public function isStrictRequirements()
109112
*/
110113
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
111114
{
112-
if (null === $route = $this->routes->get($name)) {
115+
$locale = $parameters['_locale']
116+
?? $this->context->getParameter('_locale')
117+
?: $this->defaultLocale;
118+
119+
if (null !== $locale && null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
120+
unset($parameters['_locale']);
121+
} elseif (null === $route = $this->routes->get($name)) {
113122
throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
114123
}
115124

src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\Common\Annotations\Reader;
1515
use Symfony\Component\Config\Resource\FileResource;
16+
use Symfony\Component\Routing\Annotation\Route as RouteAnnotation;
1617
use Symfony\Component\Routing\Route;
1718
use Symfony\Component\Routing\RouteCollection;
1819
use Symfony\Component\Config\Loader\LoaderInterface;
@@ -119,9 +120,11 @@ public function load($class, $type = null)
119120
}
120121
}
121122

123+
/** @var $annot RouteAnnotation */
122124
if (0 === $collection->count() && $class->hasMethod('__invoke') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
123-
$globals['path'] = '';
125+
$globals['path'] = null;
124126
$globals['name'] = '';
127+
$globals['locales'] = array();
125128
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
126129
}
127130

@@ -137,11 +140,6 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl
137140
$name = $globals['name'].$name;
138141

139142
$defaults = array_replace($globals['defaults'], $annot->getDefaults());
140-
foreach ($method->getParameters() as $param) {
141-
if (false !== strpos($globals['path'].$annot->getPath(), sprintf('{%s}', $param->getName())) && !isset($defaults[$param->getName()]) && $param->isDefaultValueAvailable()) {
142-
$defaults[$param->getName()] = $param->getDefaultValue();
143-
}
144-
}
145143
$requirements = array_replace($globals['requirements'], $annot->getRequirements());
146144
$options = array_replace($globals['options'], $annot->getOptions());
147145
$schemes = array_merge($globals['schemes'], $annot->getSchemes());
@@ -157,11 +155,57 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl
157155
$condition = $globals['condition'];
158156
}
159157

160-
$route = $this->createRoute($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
158+
$path = $annot->getLocales() ?: $annot->getPath();
159+
$prefix = $globals['locales'] ?: $globals['path'];
160+
$paths = array();
161161

162-
$this->configureRoute($route, $class, $method, $annot);
162+
if (\is_array($path)) {
163+
if (!\is_array($prefix)) {
164+
foreach ($path as $locale => $localePath) {
165+
$paths[$locale] = $prefix.$localePath;
166+
}
167+
} elseif ($missing = array_diff_key($prefix, $path)) {
168+
throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing))));
169+
} else {
170+
foreach ($path as $locale => $localePath) {
171+
if (!isset($prefix[$locale])) {
172+
throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name));
173+
}
174+
175+
$paths[$locale] = $prefix[$locale].$localePath;
176+
}
177+
}
178+
} elseif (\is_array($prefix)) {
179+
foreach ($prefix as $locale => $localePrefix) {
180+
$paths[$locale] = $localePrefix.$path;
181+
}
182+
} else {
183+
$paths[] = $prefix.$path;
184+
}
163185

164-
$collection->add($name, $route);
186+
foreach ($method->getParameters() as $param) {
187+
if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) {
188+
continue;
189+
}
190+
foreach ($paths as $locale => $path) {
191+
if (false !== strpos($path, sprintf('{%s}', $param->name))) {
192+
$defaults[$param->name] = $param->getDefaultValue();
193+
break;
194+
}
195+
}
196+
}
197+
198+
foreach ($paths as $locale => $path) {
199+
$route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
200+
$this->configureRoute($route, $class, $method, $annot);
201+
if (0 !== $locale) {
202+
$route->setDefault('_locale', $locale);
203+
$route->setDefault('_canonical_route', $name);
204+
$collection->add($name.'.'.$locale, $route);
205+
} else {
206+
$collection->add($name, $route);
207+
}
208+
}
165209
}
166210

167211
/**
@@ -208,7 +252,8 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
208252
protected function getGlobals(\ReflectionClass $class)
209253
{
210254
$globals = array(
211-
'path' => '',
255+
'path' => null,
256+
'locales' => array(),
212257
'requirements' => array(),
213258
'options' => array(),
214259
'defaults' => array(),
@@ -228,6 +273,8 @@ protected function getGlobals(\ReflectionClass $class)
228273
$globals['path'] = $annot->getPath();
229274
}
230275

276+
$globals['locales'] = $annot->getLocales();
277+
231278
if (null !== $annot->getRequirements()) {
232279
$globals['requirements'] = $annot->getRequirements();
233280
}

src/Symfony/Component/Routing/Loader/Configurator/CollectionConfigurator.php

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,25 @@ class CollectionConfigurator
2424

2525
private $parent;
2626
private $parentConfigurator;
27+
private $parentPrefixes;
2728

28-
public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null)
29+
public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null)
2930
{
3031
$this->parent = $parent;
3132
$this->name = $name;
3233
$this->collection = new RouteCollection();
3334
$this->route = new Route('');
3435
$this->parentConfigurator = $parentConfigurator; // for GC control
36+
$this->parentPrefixes = $parentPrefixes;
3537
}
3638

3739
public function __destruct()
3840
{
39-
$this->collection->addPrefix(rtrim($this->route->getPath(), '/'));
40-
$this->parent->addCollection($this->collection);
41-
}
42-
43-
/**
44-
* Adds a route.
45-
*/
46-
final public function add(string $name, string $path): RouteConfigurator
47-
{
48-
$this->collection->add($this->name.$name, $route = clone $this->route);
41+
if (null === $this->prefixes) {
42+
$this->collection->addPrefix($this->route->getPath());
43+
}
4944

50-
return new RouteConfigurator($this->collection, $route->setPath($path), $this->name, $this);
45+
$this->parent->addCollection($this->collection);
5146
}
5247

5348
/**
@@ -57,18 +52,44 @@ final public function add(string $name, string $path): RouteConfigurator
5752
*/
5853
final public function collection($name = '')
5954
{
60-
return new self($this->collection, $this->name.$name, $this);
55+
return new self($this->collection, $this->name.$name, $this, $this->prefixes);
6156
}
6257

6358
/**
6459
* Sets the prefix to add to the path of all child routes.
6560
*
61+
* @param string|array $prefix the prefix, or the localized prefixes
62+
*
6663
* @return $this
6764
*/
68-
final public function prefix(string $prefix)
65+
final public function prefix($prefix)
6966
{
70-
$this->route->setPath($prefix);
67+
if (\is_array($prefix)) {
68+
if (null === $this->parentPrefixes) {
69+
// no-op
70+
} elseif ($missing = array_diff_key($this->parentPrefixes, $prefix)) {
71+
throw new \LogicException(sprintf('Collection "%s" is missing prefixes for locale(s) "%s".', $this->name, implode('", "', array_keys($missing))));
72+
} else {
73+
foreach ($prefix as $locale => $localePrefix) {
74+
if (!isset($this->parentPrefixes[$locale])) {
75+
throw new \LogicException(sprintf('Collection "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $this->name, $locale));
76+
}
77+
78+
$prefix[$locale] = $this->parentPrefixes[$locale].$localePrefix;
79+
}
80+
}
81+
$this->prefixes = $prefix;
82+
$this->route->setPath('/');
83+
} else {
84+
$this->prefixes = null;
85+
$this->route->setPath($prefix);
86+
}
7187

7288
return $this;
7389
}
90+
91+
private function createRoute($path): Route
92+
{
93+
return (clone $this->route)->setPath($path);
94+
}
7495
}

src/Symfony/Component/Routing/Loader/Configurator/ImportConfigurator.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,36 @@ public function __destruct()
3636
/**
3737
* Sets the prefix to add to the path of all child routes.
3838
*
39+
* @param string|array $prefix the prefix, or the localized prefixes
40+
*
3941
* @return $this
4042
*/
41-
final public function prefix(string $prefix)
43+
final public function prefix($prefix)
4244
{
43-
$this->route->addPrefix($prefix);
45+
if (!\is_array($prefix)) {
46+
$this->route->addPrefix($prefix);
47+
} else {
48+
foreach ($prefix as $locale => $localePrefix) {
49+
$prefix[$locale] = trim(trim($localePrefix), '/');
50+
}
51+
foreach ($this->route->all() as $name => $route) {
52+
if (null === $locale = $route->getDefault('_locale')) {
53+
$this->route->remove($name);
54+
foreach ($prefix as $locale => $localePrefix) {
55+
$localizedRoute = clone $route;
56+
$localizedRoute->setDefault('_locale', $locale);
57+
$localizedRoute->setDefault('_canonical_route', $name);
58+
$localizedRoute->setPath($localePrefix.$route->getPath());
59+
$this->route->add($name.'.'.$locale, $localizedRoute);
60+
}
61+
} elseif (!isset($prefix[$locale])) {
62+
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
63+
} else {
64+
$route->setPath($prefix[$locale].$route->getPath());
65+
$this->route->add($name, $route);
66+
}
67+
}
68+
}
4469

4570
return $this;
4671
}

src/Symfony/Component/Routing/Loader/Configurator/RouteConfigurator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\Routing\Loader\Configurator;
1313

14-
use Symfony\Component\Routing\Route;
1514
use Symfony\Component\Routing\RouteCollection;
1615

1716
/**
@@ -24,11 +23,12 @@ class RouteConfigurator
2423

2524
private $parentConfigurator;
2625

27-
public function __construct(RouteCollection $collection, Route $route, string $name = '', CollectionConfigurator $parentConfigurator = null)
26+
public function __construct(RouteCollection $collection, $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null)
2827
{
2928
$this->collection = $collection;
3029
$this->route = $route;
3130
$this->name = $name;
3231
$this->parentConfigurator = $parentConfigurator; // for GC control
32+
$this->prefixes = $prefixes;
3333
}
3434
}

0 commit comments

Comments
 (0)