diff --git a/src/Symfony/Component/Routing/Annotation/Route.php b/src/Symfony/Component/Routing/Annotation/Route.php index 5b3cbeaab1..e9fbb654f7 100644 --- a/src/Symfony/Component/Routing/Annotation/Route.php +++ b/src/Symfony/Component/Routing/Annotation/Route.php @@ -22,6 +22,7 @@ namespace Symfony\Component\Routing\Annotation; class Route { private $path; + private $locales = array(); private $name; private $requirements = array(); private $options = array(); @@ -38,11 +39,20 @@ class Route */ public function __construct(array $data) { + if (isset($data['locales'])) { + throw new \BadMethodCallException(sprintf('Unknown property "locales" on annotation "%s".', get_class($this))); + } + if (isset($data['value'])) { - $data['path'] = $data['value']; + $data[is_array($data['value']) ? 'locales' : 'path'] = $data['value']; unset($data['value']); } + if (isset($data['path']) && is_array($data['path'])) { + $data['locales'] = $data['path']; + unset($data['path']); + } + foreach ($data as $key => $value) { $method = 'set'.str_replace('_', '', $key); if (!method_exists($this, $method)) { @@ -62,6 +72,16 @@ class Route return $this->path; } + public function setLocales(array $locales) + { + $this->locales = $locales; + } + + public function getLocales(): array + { + return $this->locales; + } + public function setHost($pattern) { $this->host = $pattern; diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index 0cb87f1163..97e0335014 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -54,11 +54,13 @@ use Psr\Log\LoggerInterface; class {$options['class']} extends {$options['base_class']} { private static \$declaredRoutes; + private \$defaultLocale; - public function __construct(RequestContext \$context, LoggerInterface \$logger = null) + public function __construct(RequestContext \$context, LoggerInterface \$logger = null, string \$defaultLocale = null) { \$this->context = \$context; \$this->logger = \$logger; + \$this->defaultLocale = \$defaultLocale; if (null === self::\$declaredRoutes) { self::\$declaredRoutes = {$this->generateDeclaredRoutes()}; } @@ -107,7 +109,14 @@ EOF; return <<<'EOF' public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) { - if (!isset(self::$declaredRoutes[$name])) { + $locale = $parameters['_locale'] + ?? $this->context->getParameter('_locale') + ?: $this->defaultLocale; + + if (null !== $locale && (self::$declaredRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) { + unset($parameters['_locale']); + $name .= '.'.$locale; + } elseif (!isset(self::$declaredRoutes[$name])) { throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); } diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index 02a59a9253..6bb8222266 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -37,6 +37,8 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt protected $logger; + private $defaultLocale; + /** * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL. * @@ -65,11 +67,12 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt '%7C' => '|', ); - public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null) + public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null, string $defaultLocale = null) { $this->routes = $routes; $this->context = $context; $this->logger = $logger; + $this->defaultLocale = $defaultLocale; } /** @@ -109,7 +112,13 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt */ public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) { - if (null === $route = $this->routes->get($name)) { + $locale = $parameters['_locale'] + ?? $this->context->getParameter('_locale') + ?: $this->defaultLocale; + + if (null !== $locale && null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) { + unset($parameters['_locale']); + } elseif (null === $route = $this->routes->get($name)) { throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); } diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php index 2fe6fb596e..b89913df8b 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Routing\Loader; use Doctrine\Common\Annotations\Reader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Config\Loader\LoaderInterface; @@ -119,9 +120,11 @@ abstract class AnnotationClassLoader implements LoaderInterface } } + /** @var $annot RouteAnnotation */ if (0 === $collection->count() && $class->hasMethod('__invoke') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { - $globals['path'] = ''; + $globals['path'] = null; $globals['name'] = ''; + $globals['locales'] = array(); $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); } @@ -137,11 +140,6 @@ abstract class AnnotationClassLoader implements LoaderInterface $name = $globals['name'].$name; $defaults = array_replace($globals['defaults'], $annot->getDefaults()); - foreach ($method->getParameters() as $param) { - if (false !== strpos($globals['path'].$annot->getPath(), sprintf('{%s}', $param->getName())) && !isset($defaults[$param->getName()]) && $param->isDefaultValueAvailable()) { - $defaults[$param->getName()] = $param->getDefaultValue(); - } - } $requirements = array_replace($globals['requirements'], $annot->getRequirements()); $options = array_replace($globals['options'], $annot->getOptions()); $schemes = array_merge($globals['schemes'], $annot->getSchemes()); @@ -157,11 +155,57 @@ abstract class AnnotationClassLoader implements LoaderInterface $condition = $globals['condition']; } - $route = $this->createRoute($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $path = $annot->getLocales() ?: $annot->getPath(); + $prefix = $globals['locales'] ?: $globals['path']; + $paths = array(); - $this->configureRoute($route, $class, $method, $annot); + if (\is_array($path)) { + if (!\is_array($prefix)) { + foreach ($path as $locale => $localePath) { + $paths[$locale] = $prefix.$localePath; + } + } elseif ($missing = array_diff_key($prefix, $path)) { + throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($prefix[$locale])) { + throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name)); + } - $collection->add($name, $route); + $paths[$locale] = $prefix[$locale].$localePath; + } + } + } elseif (\is_array($prefix)) { + foreach ($prefix as $locale => $localePrefix) { + $paths[$locale] = $localePrefix.$path; + } + } else { + $paths[] = $prefix.$path; + } + + foreach ($method->getParameters() as $param) { + if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) { + continue; + } + foreach ($paths as $locale => $path) { + if (false !== strpos($path, sprintf('{%s}', $param->name))) { + $defaults[$param->name] = $param->getDefaultValue(); + break; + } + } + } + + foreach ($paths as $locale => $path) { + $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $this->configureRoute($route, $class, $method, $annot); + if (0 !== $locale) { + $route->setDefault('_locale', $locale); + $route->setDefault('_canonical_route', $name); + $collection->add($name.'.'.$locale, $route); + } else { + $collection->add($name, $route); + } + } } /** @@ -208,7 +252,8 @@ abstract class AnnotationClassLoader implements LoaderInterface protected function getGlobals(\ReflectionClass $class) { $globals = array( - 'path' => '', + 'path' => null, + 'locales' => array(), 'requirements' => array(), 'options' => array(), 'defaults' => array(), @@ -228,6 +273,8 @@ abstract class AnnotationClassLoader implements LoaderInterface $globals['path'] = $annot->getPath(); } + $globals['locales'] = $annot->getLocales(); + if (null !== $annot->getRequirements()) { $globals['requirements'] = $annot->getRequirements(); } diff --git a/src/Symfony/Component/Routing/Loader/Configurator/CollectionConfigurator.php b/src/Symfony/Component/Routing/Loader/Configurator/CollectionConfigurator.php index 5072668ac7..e1de75e01d 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/CollectionConfigurator.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/CollectionConfigurator.php @@ -24,32 +24,27 @@ class CollectionConfigurator private $parent; private $parentConfigurator; + private $parentPrefixes; - public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null) + public function __construct(RouteCollection $parent, string $name, self $parentConfigurator = null, array $parentPrefixes = null) { $this->parent = $parent; $this->name = $name; $this->collection = new RouteCollection(); $this->route = new Route(''); $this->parentConfigurator = $parentConfigurator; // for GC control + $this->parentPrefixes = $parentPrefixes; } public function __destruct() { - $this->collection->addPrefix(rtrim($this->route->getPath(), '/')); + if (null === $this->prefixes) { + $this->collection->addPrefix($this->route->getPath()); + } + $this->parent->addCollection($this->collection); } - /** - * Adds a route. - */ - final public function add(string $name, string $path): RouteConfigurator - { - $this->collection->add($this->name.$name, $route = clone $this->route); - - return new RouteConfigurator($this->collection, $route->setPath($path), $this->name, $this); - } - /** * Creates a sub-collection. * @@ -57,18 +52,44 @@ class CollectionConfigurator */ final public function collection($name = '') { - return new self($this->collection, $this->name.$name, $this); + return new self($this->collection, $this->name.$name, $this, $this->prefixes); } /** * Sets the prefix to add to the path of all child routes. * + * @param string|array $prefix the prefix, or the localized prefixes + * * @return $this */ - final public function prefix(string $prefix) + final public function prefix($prefix) { - $this->route->setPath($prefix); + if (\is_array($prefix)) { + if (null === $this->parentPrefixes) { + // no-op + } elseif ($missing = array_diff_key($this->parentPrefixes, $prefix)) { + throw new \LogicException(sprintf('Collection "%s" is missing prefixes for locale(s) "%s".', $this->name, implode('", "', array_keys($missing)))); + } else { + foreach ($prefix as $locale => $localePrefix) { + if (!isset($this->parentPrefixes[$locale])) { + throw new \LogicException(sprintf('Collection "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $this->name, $locale)); + } + + $prefix[$locale] = $this->parentPrefixes[$locale].$localePrefix; + } + } + $this->prefixes = $prefix; + $this->route->setPath('/'); + } else { + $this->prefixes = null; + $this->route->setPath($prefix); + } return $this; } + + private function createRoute($path): Route + { + return (clone $this->route)->setPath($path); + } } diff --git a/src/Symfony/Component/Routing/Loader/Configurator/ImportConfigurator.php b/src/Symfony/Component/Routing/Loader/Configurator/ImportConfigurator.php index f978497dd2..9057c2a5d0 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/ImportConfigurator.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/ImportConfigurator.php @@ -36,11 +36,36 @@ class ImportConfigurator /** * Sets the prefix to add to the path of all child routes. * + * @param string|array $prefix the prefix, or the localized prefixes + * * @return $this */ - final public function prefix(string $prefix) + final public function prefix($prefix) { - $this->route->addPrefix($prefix); + if (!\is_array($prefix)) { + $this->route->addPrefix($prefix); + } else { + foreach ($prefix as $locale => $localePrefix) { + $prefix[$locale] = trim(trim($localePrefix), '/'); + } + foreach ($this->route->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $this->route->remove($name); + foreach ($prefix as $locale => $localePrefix) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setPath($localePrefix.$route->getPath()); + $this->route->add($name.'.'.$locale, $localizedRoute); + } + } elseif (!isset($prefix[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); + } else { + $route->setPath($prefix[$locale].$route->getPath()); + $this->route->add($name, $route); + } + } + } return $this; } diff --git a/src/Symfony/Component/Routing/Loader/Configurator/RouteConfigurator.php b/src/Symfony/Component/Routing/Loader/Configurator/RouteConfigurator.php index d0e381ad20..e700f8de7c 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/RouteConfigurator.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/RouteConfigurator.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Routing\Loader\Configurator; -use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** @@ -24,11 +23,12 @@ class RouteConfigurator private $parentConfigurator; - public function __construct(RouteCollection $collection, Route $route, string $name = '', CollectionConfigurator $parentConfigurator = null) + public function __construct(RouteCollection $collection, $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null) { $this->collection = $collection; $this->route = $route; $this->name = $name; $this->parentConfigurator = $parentConfigurator; // for GC control + $this->prefixes = $prefixes; } } diff --git a/src/Symfony/Component/Routing/Loader/Configurator/Traits/AddTrait.php b/src/Symfony/Component/Routing/Loader/Configurator/Traits/AddTrait.php index 5a3a2cd897..57dd71f2c1 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/Traits/AddTrait.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/Traits/AddTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Loader\Configurator\Traits; +use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -24,22 +25,66 @@ trait AddTrait private $name = ''; + private $prefixes; + /** * Adds a route. + * + * @param string|array $path the path, or the localized paths of the route */ - final public function add(string $name, string $path): RouteConfigurator + final public function add(string $name, $path): RouteConfigurator { - $parentConfigurator = $this instanceof RouteConfigurator ? $this->parentConfigurator : null; - $this->collection->add($this->name.$name, $route = new Route($path)); + $paths = array(); + $parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null); - return new RouteConfigurator($this->collection, $route, '', $parentConfigurator); + if (\is_array($path)) { + if (null === $this->prefixes) { + $paths = $path; + } elseif ($missing = array_diff_key($this->prefixes, $path)) { + throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($this->prefixes[$locale])) { + throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); + } + + $paths[$locale] = $this->prefixes[$locale].$localePath; + } + } + } elseif (null !== $this->prefixes) { + foreach ($this->prefixes as $locale => $prefix) { + $paths[$locale] = $prefix.$path; + } + } else { + $this->collection->add($this->name.$name, $route = $this->createRoute($path)); + + return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes); + } + + $routes = new RouteCollection(); + + foreach ($paths as $locale => $path) { + $routes->add($name.'.'.$locale, $route = $this->createRoute($path)); + $this->collection->add($this->name.$name.'.'.$locale, $route); + $route->setDefault('_locale', $locale); + $route->setDefault('_canonical_route', $this->name.$name); + } + + return new RouteConfigurator($this->collection, $routes, $this->name, $parentConfigurator, $this->prefixes); } /** * Adds a route. + * + * @param string|array $path the path, or the localized paths of the route */ - final public function __invoke(string $name, string $path): RouteConfigurator + final public function __invoke(string $name, $path): RouteConfigurator { return $this->add($name, $path); } + + private function createRoute($path): Route + { + return new Route($path); + } } diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index 31d69ca6d8..81a4c94ce0 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -107,17 +107,34 @@ class XmlFileLoader extends FileLoader */ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path) { - if ('' === ($id = $node->getAttribute('id')) || !$node->hasAttribute('path')) { - throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" and a "path" attribute.', $path)); + if ('' === $id = $node->getAttribute('id')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path)); } $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY); $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY); - list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path); + list($defaults, $requirements, $options, $condition, $paths) = $this->parseConfigs($node, $path); - $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); - $collection->add($id, $route); + if (!$paths && '' === $node->getAttribute('path')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "path" attribute or child nodes.', $path)); + } + + if ($paths && '' !== $node->getAttribute('path')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "path" attribute and child nodes.', $path)); + } + + if (!$paths) { + $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); + $collection->add($id, $route); + } else { + foreach ($paths as $locale => $p) { + $defaults['_locale'] = $locale; + $defaults['_canonical_route'] = $id; + $route = new Route($p, $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); + $collection->add($id.'.'.$locale, $route); + } + } } /** @@ -142,13 +159,42 @@ class XmlFileLoader extends FileLoader $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null; $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null; - list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path); + list($defaults, $requirements, $options, $condition, /* $paths */, $prefixes) = $this->parseConfigs($node, $path); + + if ('' !== $prefix && $prefixes) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "prefix" attribute and child nodes.', $path)); + } $this->setCurrentDir(dirname($path)); - $subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file); /* @var $subCollection RouteCollection */ - $subCollection->addPrefix($prefix); + $subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file); + + if ('' !== $prefix || !$prefixes) { + $subCollection->addPrefix($prefix); + } else { + foreach ($prefixes as $locale => $localePrefix) { + $prefixes[$locale] = trim(trim($localePrefix), '/'); + } + foreach ($subCollection->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $subCollection->remove($name); + foreach ($prefixes as $locale => $localePrefix) { + $localizedRoute = clone $route; + $localizedRoute->setPath($localePrefix.$route->getPath()); + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setDefault('_canonical_route', $name); + $subCollection->add($name.'.'.$locale, $localizedRoute); + } + } elseif (!isset($prefixes[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $path)); + } else { + $route->setPath($prefixes[$locale].$route->getPath()); + $subCollection->add($name, $route); + } + } + } + if (null !== $host) { $subCollection->setHost($host); } @@ -204,6 +250,8 @@ class XmlFileLoader extends FileLoader $requirements = array(); $options = array(); $condition = null; + $prefixes = array(); + $paths = array(); foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { if ($node !== $n->parentNode) { @@ -211,6 +259,12 @@ class XmlFileLoader extends FileLoader } switch ($n->localName) { + case 'path': + $paths[$n->getAttribute('locale')] = trim($n->textContent); + break; + case 'prefix': + $prefixes[$n->getAttribute('locale')] = trim($n->textContent); + break; case 'default': if ($this->isElementValueNull($n)) { $defaults[$n->getAttribute('key')] = null; @@ -243,7 +297,7 @@ class XmlFileLoader extends FileLoader $defaults['_controller'] = $controller; } - return array($defaults, $requirements, $options, $condition); + return array($defaults, $requirements, $options, $condition, $paths, $prefixes); } /** diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index b3ed099f81..30d66d3611 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -120,9 +120,20 @@ class YamlFileLoader extends FileLoader $defaults['_controller'] = $config['controller']; } - $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + if (is_array($config['path'])) { + $route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition); - $collection->add($name, $route); + foreach ($config['path'] as $locale => $path) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setPath($path); + $collection->add($name.'.'.$locale, $localizedRoute); + } + } else { + $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $collection->add($name, $route); + } } /** @@ -151,9 +162,34 @@ class YamlFileLoader extends FileLoader $this->setCurrentDir(dirname($path)); + /** @var RouteCollection $subCollection */ $subCollection = $this->import($config['resource'], $type, false, $file); - /* @var $subCollection RouteCollection */ - $subCollection->addPrefix($prefix); + + if (!\is_array($prefix)) { + $subCollection->addPrefix($prefix); + } else { + foreach ($prefix as $locale => $localePrefix) { + $prefix[$locale] = trim(trim($localePrefix), '/'); + } + foreach ($subCollection->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $subCollection->remove($name); + foreach ($prefix as $locale => $localePrefix) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setPath($localePrefix.$route->getPath()); + $subCollection->add($name.'.'.$locale, $localizedRoute); + } + } elseif (!isset($prefix[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $file)); + } else { + $route->setPath($prefix[$locale].$route->getPath()); + $subCollection->add($name, $route); + } + } + } + if (null !== $host) { $subCollection->setHost($host); } diff --git a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd index fd461154df..dd2477999d 100644 --- a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd +++ b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -24,6 +24,14 @@ + + + + + + + + @@ -34,10 +42,12 @@ - - + + + + - + @@ -45,8 +55,10 @@ - - + + + + diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index d1affcba9d..aa8f61cc8b 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -256,10 +256,15 @@ EOF } if (!$route->getCondition()) { + $defaults = $route->getDefaults(); + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } $default .= sprintf( "%s => array(%s, %s, %s, %s),\n", self::export($url), - self::export(array('_route' => $name) + $route->getDefaults()), + self::export(array('_route' => $name) + $defaults), self::export(!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex() ?: null), self::export(array_flip($route->getMethods()) ?: null), self::export(array_flip($route->getSchemes()) ?: null) @@ -490,10 +495,15 @@ EOF; if (!$route->getCondition() && (!is_array($next = $routes[1 + $i] ?? null) || $regex !== $next[1])) { $prevRegex = null; + $defaults = $route->getDefaults(); + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } $state->default .= sprintf( "%s => array(%s, %s, %s, %s),\n", $state->mark, - self::export(array('_route' => $name) + $route->getDefaults()), + self::export(array('_route' => $name) + $defaults), self::export($vars), self::export(array_flip($route->getMethods()) ?: null), self::export(array_flip($route->getSchemes()) ?: null) @@ -619,6 +629,11 @@ EOF; // the offset where the return value is appended below, with indendation $retOffset = 12 + strlen($code); + $defaults = $route->getDefaults(); + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } // optimize parameters array if ($matches || $hostMatches) { @@ -633,10 +648,10 @@ EOF; $code .= sprintf( " \$ret = \$this->mergeDefaults(%s, %s);\n", implode(' + ', $vars), - self::export($route->getDefaults()) + self::export($defaults) ); - } elseif ($route->getDefaults()) { - $code .= sprintf(" \$ret = %s;\n", self::export(array_replace($route->getDefaults(), array('_route' => $name)))); + } elseif ($defaults) { + $code .= sprintf(" \$ret = %s;\n", self::export(array('_route' => $name) + $defaults)); } else { $code .= sprintf(" \$ret = array('_route' => '%s');\n", $name); } diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 7b71526f46..e37cae0361 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -194,9 +194,14 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface */ protected function getAttributes(Route $route, $name, array $attributes) { + $defaults = $route->getDefaults(); + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } $attributes['_route'] = $name; - return $this->mergeDefaults($attributes, $route->getDefaults()); + return $this->mergeDefaults($attributes, $defaults); } /** diff --git a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php index 9af22f29f8..e5ae690d50 100644 --- a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php @@ -24,6 +24,14 @@ class RouteTest extends TestCase $route = new Route(array('foo' => 'bar')); } + /** + * @expectedException \BadMethodCallException + */ + public function testTryingToSetLocalesDirectly() + { + $route = new Route(array('locales' => array('nl' => 'bar'))); + } + /** * @dataProvider getValidParameters */ @@ -45,6 +53,7 @@ class RouteTest extends TestCase array('methods', array('GET', 'POST'), 'getMethods'), array('host', '{locale}.example.com', 'getHost'), array('condition', 'context.getMethod() == "GET"', 'getCondition'), + array('value', array('nl' => '/hier', 'en' => '/here'), 'getLocales'), ); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AnnotationFixtures/AbstractClassController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AnnotationFixtures/AbstractClassController.php new file mode 100644 index 0000000000..50576bcf10 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AnnotationFixtures/AbstractClassController.php @@ -0,0 +1,7 @@ + + + + + + MyBundle:Blog:show + /path + /route + + + \ No newline at end of file diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml new file mode 100644 index 0000000000..aab6a96259 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml @@ -0,0 +1,9 @@ + + + + MyBundle:Blog:show + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.yml new file mode 100644 index 0000000000..b62b569351 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale-but-not-localized.yml @@ -0,0 +1,4 @@ +--- +imported: + controller: ImportedController::someAction + path: /imported diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.xml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.xml new file mode 100644 index 0000000000..7661dbb9b0 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.xml @@ -0,0 +1,11 @@ + + + + MyBundle:Blog:show + /suffix + /le-suffix + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.yml new file mode 100644 index 0000000000..65def8a926 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/imported-with-locale.yml @@ -0,0 +1,6 @@ +--- +imported: + controller: ImportedController::someAction + path: + nl: /voorbeeld + en: /example diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-controller-default.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-controller-default.yml new file mode 100644 index 0000000000..1d13a06342 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-controller-default.yml @@ -0,0 +1,5 @@ +--- +i_need: + defaults: + _controller: DefaultController::defaultAction + resource: ./localized-route.yml diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml new file mode 100644 index 0000000000..dc3ff44dc1 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml @@ -0,0 +1,10 @@ + + + + /le-prefix + /the-prefix + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.yml new file mode 100644 index 0000000000..bc33f3f8d5 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.yml @@ -0,0 +1,6 @@ +--- +i_need: + resource: ./imported-with-locale-but-not-localized.yml + prefix: + nl: /nl + en: /en diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.xml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.xml new file mode 100644 index 0000000000..c245f6201b --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.xml @@ -0,0 +1,10 @@ + + + + /le-prefix + /the-prefix + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.yml new file mode 100644 index 0000000000..29d3571bbd --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importer-with-locale.yml @@ -0,0 +1,6 @@ +--- +i_need: + resource: ./imported-with-locale.yml + prefix: + nl: /nl + en: /en diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/importing-localized-route.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importing-localized-route.yml new file mode 100644 index 0000000000..ab54ee496e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/importing-localized-route.yml @@ -0,0 +1,3 @@ +--- +i_need: + resource: ./localized-route.yml diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/localized-route.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/localized-route.yml new file mode 100644 index 0000000000..351a418075 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/localized-route.yml @@ -0,0 +1,9 @@ +--- +home: + path: + nl: /nl + en: /en + +not_localized: + controller: HomeController::otherAction + path: /here diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/missing-locale-in-importer.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/missing-locale-in-importer.yml new file mode 100644 index 0000000000..b6d3f5ec01 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/missing-locale-in-importer.yml @@ -0,0 +1,5 @@ +--- +importing_with_missing_prefix: + resource: ./localized-route.yml + prefix: + nl: /prefix diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/not-localized.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/not-localized.yml new file mode 100644 index 0000000000..4be493da02 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/not-localized.yml @@ -0,0 +1,4 @@ +--- +not_localized: + controller: string + path: /here diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/officially_formatted_locales.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/officially_formatted_locales.yml new file mode 100644 index 0000000000..a125a4efe8 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/officially_formatted_locales.yml @@ -0,0 +1,7 @@ +--- +official: + controller: HomeController::someAction + path: + fr.UTF-8: /omelette-au-fromage + pt-PT: /eu-não-sou-espanhol + pt_BR: /churrasco diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/localized/route-without-path-or-locales.yml b/src/Symfony/Component/Routing/Tests/Fixtures/localized/route-without-path-or-locales.yml new file mode 100644 index 0000000000..4c7c599f3d --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/localized/route-without-path-or-locales.yml @@ -0,0 +1,3 @@ +--- +routename: + controller: Here::here diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_i18n.php b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_i18n.php new file mode 100644 index 0000000000..ed4a0e22e1 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_i18n.php @@ -0,0 +1,17 @@ +collection() + ->prefix(array('en' => '/glish')) + ->add('foo', '/foo') + ->add('bar', array('en' => '/bar')); + + $routes + ->add('baz', array('en' => '/baz')); + + $routes->import('php_dsl_sub_i18n.php') + ->prefix(array('fr' => '/ench')); +}; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_sub_i18n.php b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_sub_i18n.php new file mode 100644 index 0000000000..c112e716ce --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl_sub_i18n.php @@ -0,0 +1,11 @@ +collection('c_') + ->prefix('pub'); + + $add('foo', array('fr' => '/foo')); + $add('bar', array('fr' => '/bar')); +}; diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index 4b2e5b196d..dc84e29345 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -84,6 +84,33 @@ class PhpGeneratorDumperTest extends TestCase $this->assertEquals('/app.php/testing2', $relativeUrlWithoutParameter); } + public function testDumpWithLocalizedRoutes() + { + $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')); + + $code = $this->generatorDumper->dump([ + 'class' => 'LocalizedProjectUrlGenerator', + ]); + file_put_contents($this->testTmpFilepath, $code); + include $this->testTmpFilepath; + + $context = new RequestContext('/app.php'); + $projectUrlGenerator = new \LocalizedProjectUrlGenerator($context, null, 'en'); + + $urlWithDefaultLocale = $projectUrlGenerator->generate('test'); + $urlWithSpecifiedLocale = $projectUrlGenerator->generate('test', ['_locale' => 'nl']); + $context->setParameter('_locale', 'en'); + $urlWithEnglishContext = $projectUrlGenerator->generate('test'); + $context->setParameter('_locale', 'nl'); + $urlWithDutchContext = $projectUrlGenerator->generate('test'); + + $this->assertEquals('/app.php/testing/is/fun', $urlWithDefaultLocale); + $this->assertEquals('/app.php/testen/is/leuk', $urlWithSpecifiedLocale); + $this->assertEquals('/app.php/testing/is/fun', $urlWithEnglishContext); + $this->assertEquals('/app.php/testen/is/leuk', $urlWithDutchContext); + } + public function testDumpWithTooManyRoutes() { $this->routeCollection->add('Test', new Route('/testing/{foo}')); diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php index 70db1ccd9a..14e634ca71 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php @@ -11,35 +11,44 @@ namespace Symfony\Component\Routing\Tests\Loader; -use Symfony\Component\Routing\Annotation\Route; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Loader\AnnotationClassLoader; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\AbstractClassController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\ActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\DefaultValueController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\ExplicitLocalizedActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\InvokableController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\InvokableLocalizedController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedMethodActionControllers; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixLocalizedActionController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixMissingLocaleActionController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixMissingRouteLocaleActionController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\LocalizedPrefixWithRouteWithoutLocale; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\MethodActionControllers; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\MissingRouteNameController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\NothingButNameController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionLocalizedRouteController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\RouteWithPrefixController; -class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest +class AnnotationClassLoaderTest extends TestCase { - protected $loader; - private $reader; + /** + * @var AnnotationClassLoader + */ + private $loader; protected function setUp() { - parent::setUp(); - - $this->reader = $this->getReader(); - $this->loader = $this->getClassLoader($this->reader); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testLoadMissingClass() - { - $this->loader->load('MissingClass'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testLoadAbstractClass() - { - $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\AbstractClass'); + $reader = new AnnotationReader(); + $this->loader = new class($reader) extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) {} + }; + AnnotationRegistry::registerLoader('class_exists'); } /** @@ -69,187 +78,144 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest $this->assertFalse($this->loader->supports('class', 'foo'), '->supports() checks the resource type if specified'); } - public function getLoadTests() + public function testSimplePathRoute() { - return array( - array( - 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', - array('name' => 'route1', 'path' => '/path'), - array('arg2' => 'defaultValue2', 'arg3' => 'defaultValue3'), - ), - array( - 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', - array('defaults' => array('arg2' => 'foo'), 'requirements' => array('arg3' => '\w+')), - array('arg2' => 'defaultValue2', 'arg3' => 'defaultValue3'), - ), - array( - 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', - array('options' => array('foo' => 'bar')), - array('arg2' => 'defaultValue2', 'arg3' => 'defaultValue3'), - ), - array( - 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', - array('schemes' => array('https'), 'methods' => array('GET')), - array('arg2' => 'defaultValue2', 'arg3' => 'defaultValue3'), - ), - array( - 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', - array('condition' => 'context.getMethod() == "GET"'), - array('arg2' => 'defaultValue2', 'arg3' => 'defaultValue3'), - ), - ); + $routes = $this->loader->load(ActionPathController::class); + $this->assertCount(1, $routes); + $this->assertEquals('/path', $routes->get('action')->getPath()); } - /** - * @dataProvider getLoadTests - */ - public function testLoad($className, $routeData = array(), $methodArgs = array()) + public function testInvokableControllerLoader() { - $routeData = array_replace(array( - 'name' => 'route', - 'path' => '/', - 'requirements' => array(), - 'options' => array(), - 'defaults' => array(), - 'schemes' => array(), - 'methods' => array(), - 'condition' => '', - ), $routeData); - - $this->reader - ->expects($this->once()) - ->method('getMethodAnnotations') - ->will($this->returnValue(array($this->getAnnotatedRoute($routeData)))) - ; - - $routeCollection = $this->loader->load($className); - $route = $routeCollection->get($routeData['name']); - - $this->assertSame($routeData['path'], $route->getPath(), '->load preserves path annotation'); - $this->assertCount( - count($routeData['requirements']), - array_intersect_assoc($routeData['requirements'], $route->getRequirements()), - '->load preserves requirements annotation' - ); - $this->assertCount( - count($routeData['options']), - array_intersect_assoc($routeData['options'], $route->getOptions()), - '->load preserves options annotation' - ); - $this->assertCount( - count($routeData['defaults']), - $route->getDefaults(), - '->load preserves defaults annotation' - ); - $this->assertEquals($routeData['schemes'], $route->getSchemes(), '->load preserves schemes annotation'); - $this->assertEquals($routeData['methods'], $route->getMethods(), '->load preserves methods annotation'); - $this->assertSame($routeData['condition'], $route->getCondition(), '->load preserves condition annotation'); + $routes = $this->loader->load(InvokableController::class); + $this->assertCount(1, $routes); + $this->assertEquals('/here', $routes->get('lol')->getPath()); } - public function testClassRouteLoad() + public function testInvokableLocalizedControllerLoading() { - $classRouteData = array( - 'name' => 'prefix_', - 'path' => '/prefix', - 'schemes' => array('https'), - 'methods' => array('GET'), - ); - - $methodRouteData = array( - 'name' => 'route1', - 'path' => '/path', - 'schemes' => array('http'), - 'methods' => array('POST', 'PUT'), - ); - - $this->reader - ->expects($this->once()) - ->method('getClassAnnotation') - ->will($this->returnValue($this->getAnnotatedRoute($classRouteData))) - ; - $this->reader - ->expects($this->once()) - ->method('getMethodAnnotations') - ->will($this->returnValue(array($this->getAnnotatedRoute($methodRouteData)))) - ; - - $routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass'); - $route = $routeCollection->get($classRouteData['name'].$methodRouteData['name']); - - $this->assertSame($classRouteData['path'].$methodRouteData['path'], $route->getPath(), '->load concatenates class and method route path'); - $this->assertEquals(array_merge($classRouteData['schemes'], $methodRouteData['schemes']), $route->getSchemes(), '->load merges class and method route schemes'); - $this->assertEquals(array_merge($classRouteData['methods'], $methodRouteData['methods']), $route->getMethods(), '->load merges class and method route methods'); + $routes = $this->loader->load(InvokableLocalizedController::class); + $this->assertCount(2, $routes); + $this->assertEquals('/here', $routes->get('action.en')->getPath()); + $this->assertEquals('/hier', $routes->get('action.nl')->getPath()); } - public function testInvokableClassRouteLoad() + public function testLocalizedPathRoutes() { - $classRouteData = array( - 'name' => 'route1', - 'path' => '/', - 'schemes' => array('https'), - 'methods' => array('GET'), - ); - - $this->reader - ->expects($this->exactly(2)) - ->method('getClassAnnotation') - ->will($this->returnValue($this->getAnnotatedRoute($classRouteData))) - ; - $this->reader - ->expects($this->once()) - ->method('getMethodAnnotations') - ->will($this->returnValue(array())) - ; - - $routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass'); - $route = $routeCollection->get($classRouteData['name']); - - $this->assertSame($classRouteData['path'], $route->getPath(), '->load preserves class route path'); - $this->assertEquals(array_merge($classRouteData['schemes'], $classRouteData['schemes']), $route->getSchemes(), '->load preserves class route schemes'); - $this->assertEquals(array_merge($classRouteData['methods'], $classRouteData['methods']), $route->getMethods(), '->load preserves class route methods'); + $routes = $this->loader->load(LocalizedActionPathController::class); + $this->assertCount(2, $routes); + $this->assertEquals('/path', $routes->get('action.en')->getPath()); + $this->assertEquals('/pad', $routes->get('action.nl')->getPath()); } - public function testInvokableClassWithMethodRouteLoad() + public function testLocalizedPathRoutesWithExplicitPathPropety() { - $classRouteData = array( - 'name' => 'route1', - 'path' => '/prefix', - 'schemes' => array('https'), - 'methods' => array('GET'), - ); - - $methodRouteData = array( - 'name' => 'route2', - 'path' => '/path', - 'schemes' => array('http'), - 'methods' => array('POST', 'PUT'), - ); - - $this->reader - ->expects($this->once()) - ->method('getClassAnnotation') - ->will($this->returnValue($this->getAnnotatedRoute($classRouteData))) - ; - $this->reader - ->expects($this->once()) - ->method('getMethodAnnotations') - ->will($this->returnValue(array($this->getAnnotatedRoute($methodRouteData)))) - ; - - $routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass'); - $route = $routeCollection->get($classRouteData['name']); - - $this->assertNull($route, '->load ignores class route'); - - $route = $routeCollection->get($classRouteData['name'].$methodRouteData['name']); - - $this->assertSame($classRouteData['path'].$methodRouteData['path'], $route->getPath(), '->load concatenates class and method route path'); - $this->assertEquals(array_merge($classRouteData['schemes'], $methodRouteData['schemes']), $route->getSchemes(), '->load merges class and method route schemes'); - $this->assertEquals(array_merge($classRouteData['methods'], $methodRouteData['methods']), $route->getMethods(), '->load merges class and method route methods'); + $routes = $this->loader->load(ExplicitLocalizedActionPathController::class); + $this->assertCount(2, $routes); + $this->assertEquals('/path', $routes->get('action.en')->getPath()); + $this->assertEquals('/pad', $routes->get('action.nl')->getPath()); } - private function getAnnotatedRoute($data) + public function testDefaultValuesForMethods() { - return new Route($data); + $routes = $this->loader->load(DefaultValueController::class); + $this->assertCount(1, $routes); + $this->assertEquals('/{default}/path', $routes->get('action')->getPath()); + $this->assertEquals('value', $routes->get('action')->getDefault('default')); + } + + public function testMethodActionControllers() + { + $routes = $this->loader->load(MethodActionControllers::class); + $this->assertCount(2, $routes); + $this->assertEquals('/the/path', $routes->get('put')->getPath()); + $this->assertEquals('/the/path', $routes->get('post')->getPath()); + } + + public function testLocalizedMethodActionControllers() + { + $routes = $this->loader->load(LocalizedMethodActionControllers::class); + $this->assertCount(4, $routes); + $this->assertEquals('/the/path', $routes->get('put.en')->getPath()); + $this->assertEquals('/the/path', $routes->get('post.en')->getPath()); + } + + public function testRouteWithPathWithPrefix() + { + $routes = $this->loader->load(PrefixedActionPathController::class); + $this->assertCount(1, $routes); + $route = $routes->get('action'); + $this->assertEquals('/prefix/path', $route->getPath()); + $this->assertEquals('lol=fun', $route->getCondition()); + $this->assertEquals('frankdejonge.nl', $route->getHost()); + } + + public function testLocalizedRouteWithPathWithPrefix() + { + $routes = $this->loader->load(PrefixedActionLocalizedRouteController::class); + $this->assertCount(2, $routes); + $this->assertEquals('/prefix/path', $routes->get('action.en')->getPath()); + $this->assertEquals('/prefix/pad', $routes->get('action.nl')->getPath()); + } + + public function testLocalizedPrefixLocalizedRoute() + { + $routes = $this->loader->load(LocalizedPrefixLocalizedActionController::class); + $this->assertCount(2, $routes); + $this->assertEquals('/nl/actie', $routes->get('action.nl')->getPath()); + $this->assertEquals('/en/action', $routes->get('action.en')->getPath()); + } + + public function testMissingPrefixLocale() + { + $this->expectException(\LogicException::class); + $this->loader->load(LocalizedPrefixMissingLocaleActionController::class); + } + + public function testMissingRouteLocale() + { + $this->expectException(\LogicException::class); + $this->loader->load(LocalizedPrefixMissingRouteLocaleActionController::class); + } + + public function testRouteWithoutName() + { + $routes = $this->loader->load(MissingRouteNameController::class)->all(); + $this->assertCount(1, $routes); + $this->assertEquals('/path', reset($routes)->getPath()); + } + + public function testNothingButName() + { + $routes = $this->loader->load(NothingButNameController::class)->all(); + $this->assertCount(1, $routes); + $this->assertEquals('/', reset($routes)->getPath()); + } + + public function testNonExistingClass() + { + $this->expectException(\LogicException::class); + $this->loader->load('ClassThatDoesNotExist'); + } + + public function testLoadingAbstractClass() + { + $this->expectException(\LogicException::class); + $this->loader->load(AbstractClassController::class); + } + + public function testLocalizedPrefixWithoutRouteLocale() + { + $routes = $this->loader->load(LocalizedPrefixWithRouteWithoutLocale::class); + $this->assertCount(2, $routes); + $this->assertEquals('/en/suffix', $routes->get('action.en')->getPath()); + $this->assertEquals('/nl/suffix', $routes->get('action.nl')->getPath()); + } + + public function testLoadingRouteWithPrefix() + { + $routes = $this->loader->load(RouteWithPrefixController::class); + $this->assertCount(1, $routes); + $this->assertEquals('/prefix/path', $routes->get('action')->getPath()); } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/FileLocatorStub.php b/src/Symfony/Component/Routing/Tests/Loader/FileLocatorStub.php new file mode 100644 index 0000000000..870c3cf4f4 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Loader/FileLocatorStub.php @@ -0,0 +1,17 @@ +assertEquals($expectedCollection, $routeCollection); } + + public function testRoutingI18nConfigurator() + { + $locator = new FileLocator(array(__DIR__.'/../Fixtures')); + $loader = new PhpFileLoader($locator); + $routeCollection = $loader->load('php_dsl_i18n.php'); + + $expectedCollection = new RouteCollection(); + + $expectedCollection->add('foo.en', (new Route('/glish/foo'))->setDefaults(array('_locale' => 'en', '_canonical_route' => 'foo'))); + $expectedCollection->add('bar.en', (new Route('/glish/bar'))->setDefaults(array('_locale' => 'en', '_canonical_route' => 'bar'))); + $expectedCollection->add('baz.en', (new Route('/baz'))->setDefaults(array('_locale' => 'en', '_canonical_route' => 'baz'))); + $expectedCollection->add('c_foo.fr', (new Route('/ench/pub/foo'))->setDefaults(array('_locale' => 'fr', '_canonical_route' => 'c_foo'))); + $expectedCollection->add('c_bar.fr', (new Route('/ench/pub/bar'))->setDefaults(array('_locale' => 'fr', '_canonical_route' => 'c_bar'))); + + $expectedCollection->addResource(new FileResource(realpath(__DIR__.'/../Fixtures/php_dsl_sub_i18n.php'))); + $expectedCollection->addResource(new FileResource(realpath(__DIR__.'/../Fixtures/php_dsl_i18n.php'))); + + $this->assertEquals($expectedCollection, $routeCollection); + } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index e5353d7eba..0b2e1a9d79 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -83,6 +83,45 @@ class XmlFileLoaderTest extends TestCase } } + public function testLoadLocalized() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures'))); + $routeCollection = $loader->load('localised.xml'); + $routes = $routeCollection->all(); + + $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + + $this->assertEquals('/route', $routeCollection->get('localised.fr')->getPath()); + $this->assertEquals('/path', $routeCollection->get('localised.en')->getPath()); + } + + public function testLocalisedImports() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routeCollection = $loader->load('importer-with-locale.xml'); + $routes = $routeCollection->all(); + + $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + + $this->assertEquals('/le-prefix/le-suffix', $routeCollection->get('imported.fr')->getPath()); + $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); + } + + public function testLocalisedImportsOfNotLocalizedRoutes() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routeCollection = $loader->load('importer-with-locale-imports-non-localized-route.xml'); + $routes = $routeCollection->all(); + + $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + + $this->assertEquals('/le-prefix/suffix', $routeCollection->get('imported.fr')->getPath()); + $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); + } + /** * @expectedException \InvalidArgumentException * @dataProvider getPathsToInvalidFiles diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index 5fa38f39d0..3bcfe1b5b6 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -193,4 +193,86 @@ class YamlFileLoaderTest extends TestCase $this->assertNotNull($routeCollection->get('api_app_blog')); $this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath()); } + + public function testRemoteSourcesAreNotAccepted() + { + $loader = new YamlFileLoader(new FileLocatorStub()); + $this->expectException(\InvalidArgumentException::class); + $loader->load('http://remote.com/here.yml'); + } + + public function testLoadingLocalizedRoute() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('localized-route.yml'); + + $this->assertCount(3, $routes); + } + + + public function testImportingRoutesFromDefinition() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('importing-localized-route.yml'); + + $this->assertCount(3, $routes); + $this->assertEquals('/nl', $routes->get('home.nl')->getPath()); + $this->assertEquals('/en', $routes->get('home.en')->getPath()); + $this->assertEquals('/here', $routes->get('not_localized')->getPath()); + } + + public function testImportingRoutesWithLocales() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('importer-with-locale.yml'); + + $this->assertCount(2, $routes); + $this->assertEquals('/nl/voorbeeld', $routes->get('imported.nl')->getPath()); + $this->assertEquals('/en/example', $routes->get('imported.en')->getPath()); + } + + public function testImportingNonLocalizedRoutesWithLocales() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('importer-with-locale-imports-non-localized-route.yml'); + + $this->assertCount(2, $routes); + $this->assertEquals('/nl/imported', $routes->get('imported.nl')->getPath()); + $this->assertEquals('/en/imported', $routes->get('imported.en')->getPath()); + } + + public function testImportingRoutesWithOfficialLocales() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('officially_formatted_locales.yml'); + + $this->assertCount(3, $routes); + $this->assertEquals('/omelette-au-fromage', $routes->get('official.fr.UTF-8')->getPath()); + $this->assertEquals('/eu-não-sou-espanhol', $routes->get('official.pt-PT')->getPath()); + $this->assertEquals('/churrasco', $routes->get('official.pt_BR')->getPath()); + } + + public function testImportingRoutesFromDefinitionMissingLocalePrefix() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $this->expectException(\InvalidArgumentException::class); + $loader->load('missing-locale-in-importer.yml'); + } + + public function testImportingRouteWithoutPathOrLocales() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $this->expectException(\InvalidArgumentException::class); + $loader->load('route-without-path-or-locales.yml'); + } + + public function testImportingWithControllerDefault() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/localized'))); + $routes = $loader->load('importer-with-controller-default.yml'); + $this->assertCount(3, $routes); + $this->assertEquals('DefaultController::defaultAction', $routes->get('home.en')->getDefault('_controller')); + $this->assertEquals('DefaultController::defaultAction', $routes->get('home.nl')->getDefault('_controller')); + $this->assertEquals('DefaultController::defaultAction', $routes->get('not_localized')->getDefault('_controller')); + } }