Skip to content

Commit 2378358

Browse files
frankdejongenicolas-grekas
authored andcommitted
[Routing] Implement i18n routing
1 parent 5965917 commit 2378358

File tree

54 files changed

+1109
-235
lines changed

Some content is hidden

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

54 files changed

+1109
-235
lines changed

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;

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 && isset(self::$declaredRoutes[$name.'.'.$locale])) {
117+
unset($parameters['_locale']);
118+
$name = $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

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)) {
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

Loader/AnnotationClassLoader.php

Lines changed: 56 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,56 @@ 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+
$collection->add($name.'.'.$locale, $route);
204+
} else {
205+
$collection->add($name, $route);
206+
}
207+
}
165208
}
166209

167210
/**
@@ -208,7 +251,8 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho
208251
protected function getGlobals(\ReflectionClass $class)
209252
{
210253
$globals = array(
211-
'path' => '',
254+
'path' => null,
255+
'locales' => array(),
212256
'requirements' => array(),
213257
'options' => array(),
214258
'defaults' => array(),
@@ -228,6 +272,8 @@ protected function getGlobals(\ReflectionClass $class)
228272
$globals['path'] = $annot->getPath();
229273
}
230274

275+
$globals['locales'] = $annot->getLocales();
276+
231277
if (null !== $annot->getRequirements()) {
232278
$globals['requirements'] = $annot->getRequirements();
233279
}

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
}

Loader/Configurator/ImportConfigurator.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,35 @@ 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->setPath($localePrefix.$route->getPath());
58+
$this->route->add($name.'.'.$locale, $localizedRoute);
59+
}
60+
} elseif (!isset($prefix[$locale])) {
61+
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
62+
} else {
63+
$route->setPath($prefix[$locale].$route->getPath());
64+
$this->route->add($name, $route);
65+
}
66+
}
67+
}
4468

4569
return $this;
4670
}

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)