├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── composer.json ├── phpunit.xml.dist ├── readme.md ├── src ├── DependencyInjection │ ├── CompilerPass │ │ └── ReplaceRouterCompilerPass.php │ ├── Configuration.php │ └── I18nRoutingExtension.php ├── I18nRoutingBundle.php ├── Resources │ └── config │ │ ├── annotations.php │ │ └── services.php └── Routing │ ├── Annotation │ └── I18nRoute.php │ ├── I18nRouter.php │ └── Loader │ ├── AnnotatedI18nRouteLoader.php │ ├── MissingRouteLocale.php │ ├── MissingRoutePath.php │ └── YamlFileLoader.php └── tests ├── AnnotatedI18nRouteLoaderTest.php ├── AnnotationFixtures ├── AbstractClassController.php ├── ActionPathController.php ├── DefaultValueController.php ├── InvokableController.php ├── InvokableLocalizedController.php ├── LocalizedActionPathController.php ├── LocalizedMethodActionControllers.php ├── LocalizedPrefixLocalizedActionController.php ├── LocalizedPrefixMissingLocaleActionController.php ├── LocalizedPrefixMissingRouteLocaleActionController.php ├── LocalizedPrefixWithRouteWithoutLocale.php ├── MethodActionControllers.php ├── MissingRouteNameController.php ├── NothingButNameController.php ├── PrefixedActionLocalizedRouteController.php ├── PrefixedActionPathController.php └── SymfonyRouteWithPrefixController.php ├── FileLocatorStub.php ├── I18nRouteTest.php ├── I18nRoutingBundleTest.php ├── I18nRoutingExtensionTest.php ├── I18nRoutingRouterTest.php ├── RouterStub.php ├── YamlFileLoaderTest.php └── fixtures ├── empty.yml ├── imported-with-locale.yml ├── importer-with-all-options.yml ├── importer-with-controller-default.yml ├── importer-with-locale.yml ├── importing-localized-route.yml ├── importing-not-localized-with-localized-prefix.yml ├── invalid.yml ├── localized-route.yml ├── missing-locale-in-importer.yml ├── not-an-array.yml ├── not-localized.yml ├── route-has-too-many-properties.yml ├── route-is-not-an-array.yml ├── route-with-2-controllers.yml ├── route-with-resource.yml ├── route-with-type.yml └── route-without-path-or-locales.yml /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | coverage/ 3 | composer.lock -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | excluded_paths: [tests/*, coverage/*] 4 | checks: 5 | php: 6 | code_rating: true 7 | remove_extra_empty_lines: true 8 | remove_php_closing_tag: true 9 | remove_trailing_whitespace: true 10 | fix_use_statements: 11 | remove_unused: true 12 | preserve_multiple: false 13 | preserve_blanklines: true 14 | order_alphabetically: true 15 | fix_php_opening_tag: true 16 | fix_linefeed: true 17 | fix_line_ending: true 18 | fix_identation_4spaces: true 19 | fix_doc_comments: true 20 | tools: 21 | external_code_coverage: 22 | timeout: 1200 23 | runs: 2 24 | php_code_coverage: false 25 | php_code_sniffer: 26 | config: 27 | standard: PSR2 28 | filter: 29 | paths: ['src'] 30 | php_loc: 31 | enabled: true 32 | excluded_dirs: [vendor, tests] 33 | php_sim: false 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | install: 8 | - travis_retry composer install 9 | 10 | script: 11 | - vendor/bin/phpunit --coverage-text --coverage-clover coverage.xml 12 | 13 | after_script: 14 | - wget https://scrutinizer-ci.com/ocular.phar 15 | - php ocular.phar code-coverage:upload --format=php-clover coverage.xml 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frankdejonge/symfony-i18n-routing", 3 | "description": "Internationalised routing for Symfony 4", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "FrankDeJonge\\SymfonyI18nRouting\\": "src" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "FrankDeJonge\\SymfonyI18nRouting\\AnnotationFixtures\\": "tests/AnnotationFixtures" 14 | } 15 | }, 16 | "require": { 17 | "php": "^7.1.3", 18 | "symfony/routing": "^4.0", 19 | "symfony/config": "^4.0" 20 | }, 21 | "require-dev": { 22 | "symfony/dependency-injection": "^4.0", 23 | "phpunit/phpunit": "^6.0", 24 | "symfony/http-kernel": "^4.0", 25 | "symfony/yaml": "^4.0", 26 | "matthiasnoback/symfony-dependency-injection-test": "^2.3", 27 | "doctrine/annotations": "^1.6", 28 | "nyholm/symfony-bundle-test": "^1.3" 29 | }, 30 | "authors": [ 31 | { 32 | "name": "Frank de Jonge", 33 | "email": "info@frenky.net" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/ 6 | src/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/ 13 | 14 | src/* 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Internationalized routing for Symfony 4 2 | 3 | > This bundle provides i18n routing for Symfony 4. 4 | 5 | [![Author](https://img.shields.io/badge/author-@frankdejonge-blue.svg?style=flat-square)](https://twitter.com/frankdejonge) 6 | [![Build Status](https://img.shields.io/travis/frankdejonge/symfony-i18n-routing/master.svg?style=flat-square)](https://travis-ci.org/frankdejonge/symfony-i18n-routing) 7 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/frankdejonge/symfony-i18n-routing.svg?style=flat-square)](https://scrutinizer-ci.com/g/frankdejonge/symfony-i18n-routing/code-structure) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 9 | [![Packagist Version](https://img.shields.io/packagist/v/frankdejonge/symfony-i18n-routing.svg?style=flat-square)](https://packagist.org/packages/frankdejonge/symfony-i18n-routing) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/frankdejonge/symfony-i18n-routing.svg?style=flat-square)](https://packagist.org/packages/frankdejonge/symfony-i18n-routing) 11 | 12 | ## Purpose 13 | 14 | This bundle provides a method of internationalization of route definitions. This means 15 | you can define a path per locale and still have them route to the same controller action. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | composer req frankdejonge/symfony-i18n-routing 21 | ``` 22 | 23 | Register the bundle in `bundles.php` 24 | 25 | ```php 26 | ['all' => true], 30 | // ... 31 | ]; 32 | ``` 33 | 34 | Note that if you want to use the annotations you'll need to ensure this bundle is loaded BEFORE the FrameworkBundle. 35 | 36 | ## Configuration 37 | 38 | ```yaml 39 | frankdejonge_i18n_routing: 40 | default_locale: en 41 | use_annotations: false # set to true to enable annotation loading 42 | ``` 43 | 44 | ## Yaml usage 45 | 46 | From your main `config/routes.yml` import your localized routes: 47 | 48 | ```yaml 49 | i18n_routes: 50 | resource: ./i18n_routes/routes.yml 51 | type: i18n_routes 52 | ``` 53 | 54 | Now you can define i18n routes in `config/i18n_routes/routes.yml`: 55 | 56 | ```yaml 57 | contact: 58 | controller: ContactController::formAction 59 | locales: 60 | en: /send-us-an-email 61 | nl: /stuur-ons-een-email 62 | ``` 63 | 64 | This is effectively the same as defining: 65 | 66 | ```yaml 67 | contact.en: 68 | controller: ContactController::formAction 69 | path: /send-us-an-email 70 | defaults: 71 | _locale: en 72 | 73 | contact.nl: 74 | controller: ContactController::formAction 75 | path: /stuur-ons-een-email 76 | defaults: 77 | _locale: nl 78 | ``` 79 | 80 | As you can see this saves you a bit of typing and prevents you from 81 | having to keep 2 definitions in sync (less error prone). 82 | 83 | ## Annotation usage 84 | 85 | The annotation loader supports both normal route annotations and 86 | localized ones. The `@I18nROute` and `@Route` annotations can be 87 | be mixed at will. 88 | 89 | ```php 90 | generate('contact'); 128 | $urlWithSpecifiedLocale = $urlGenerator->generate('contact', ['_locale' => 'nl']); 129 | ``` 130 | -------------------------------------------------------------------------------- /src/DependencyInjection/CompilerPass/ReplaceRouterCompilerPass.php: -------------------------------------------------------------------------------- 1 | setAlias('router', 'frankdejonge_i18n_routing.router') 17 | ->setPublic('true'); 18 | } 19 | } -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('frankdejonge_i18n_routing'); 14 | $root->children() 15 | ->scalarNode('default_locale') 16 | ->defaultValue('en') 17 | ->end() 18 | ->booleanNode('use_annotations') 19 | ->defaultFalse() 20 | ->end() 21 | ->end(); 22 | 23 | return $treeBuilder; 24 | } 25 | } -------------------------------------------------------------------------------- /src/DependencyInjection/I18nRoutingExtension.php: -------------------------------------------------------------------------------- 1 | load('services.php'); 32 | $config = $this->processConfiguration(new Configuration(), $configs); 33 | $container->setParameter('frankdejonge_i18n_routing.default_locale', $config['default_locale']); 34 | $this->configureAnnotationLoader($config, $loader); 35 | } 36 | 37 | private function configureAnnotationLoader(array $config, LoaderInterface $loader) 38 | { 39 | if ($config['use_annotations'] ?? false) { 40 | $loader->load('annotations.php'); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/I18nRoutingBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new ReplaceRouterCompilerPass()); 16 | } 17 | 18 | 19 | public function getContainerExtension() 20 | { 21 | return new I18nRoutingExtension(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Resources/config/annotations.php: -------------------------------------------------------------------------------- 1 | register('frankdejonge_i18n_routing.annotation.directory_loader', AnnotationDirectoryLoader::class) 9 | ->addTag('routing.loader') 10 | ->setArguments([ 11 | new Reference('file_locator'), 12 | new Reference('frankdejonge_i18n_routing.annotation.class_loader') 13 | ]); 14 | 15 | $container->register('frankdejonge_i18n_routing.annotation.file_loader', AnnotationFileLoader::class) 16 | ->addTag('routing.loader') 17 | ->setArguments([ 18 | new Reference('file_locator'), 19 | new Reference('frankdejonge_i18n_routing.annotation.class_loader') 20 | ]); 21 | 22 | $container->register('frankdejonge_i18n_routing.annotation.class_loader', AnnotatedI18nRouteLoader::class) 23 | ->addTag('routing.loader') 24 | ->setArguments([ 25 | new Reference('annotation_reader') 26 | ]); -------------------------------------------------------------------------------- /src/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | register('frankdejonge_i18n_routing.yaml_loader', YamlFileLoader::class) 8 | ->addTag('routing.loader') 9 | ->addArgument(new Reference('file_locator')); 10 | 11 | $container->register('frankdejonge_i18n_routing.router', I18nRouter::class) 12 | ->addArgument(new Reference('router.default')) 13 | ->addArgument('%frankdejonge_i18n_routing.default_locale%'); -------------------------------------------------------------------------------- /src/Routing/Annotation/I18nRoute.php: -------------------------------------------------------------------------------- 1 | locales; 31 | } 32 | 33 | public function setLocales(array $locales) 34 | { 35 | $this->locales = $locales; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Routing/I18nRouter.php: -------------------------------------------------------------------------------- 1 | router = $router; 30 | $this->defaultLocale = $defaultLocale; 31 | } 32 | 33 | public function setContext(RequestContext $context) 34 | { 35 | $this->router->setContext($context); 36 | } 37 | 38 | public function getContext() 39 | { 40 | return $this->router->getContext(); 41 | } 42 | 43 | public function getRouteCollection() 44 | { 45 | return $this->router->getRouteCollection(); 46 | } 47 | 48 | /** 49 | * Generates a URL or path for a specific route based on the given parameters. 50 | * 51 | * Parameters that reference placeholders in the route pattern will substitute them in the 52 | * path or host. Extra params are added as query string to the URL. 53 | * 54 | * When the passed reference type cannot be generated for the route because it requires a different 55 | * host or scheme than the current one, the method will return a more comprehensive reference 56 | * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH 57 | * but the route requires the https scheme whereas the current scheme is http, it will instead return an 58 | * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches 59 | * the route in any case. 60 | * 61 | * If there is no route with the given name, the generator must throw the RouteNotFoundException. 62 | * 63 | * The special parameter _fragment will be used as the document fragment suffixed to the final URL. 64 | * 65 | * @param string $name The name of the route 66 | * @param mixed $parameters An array of parameters 67 | * @param int $referenceType The type of reference to be generated (one of the constants) 68 | * 69 | * @return string The generated URL 70 | * 71 | * @throws RouteNotFoundException If the named route doesn't exist 72 | * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route 73 | * @throws InvalidParameterException When a parameter value for a placeholder is not correct because 74 | * it does not match the requirement 75 | */ 76 | public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) 77 | { 78 | $locale = $parameters['_locale'] 79 | ?? $this->router->getContext()->getParameter('_locale') 80 | ?: $this->defaultLocale; 81 | $i18nParameters = $parameters; 82 | unset($i18nParameters['_locale']); 83 | $i18nRouteName = "{$name}.{$locale}"; 84 | 85 | try { 86 | return $this->router->generate($i18nRouteName, $i18nParameters, $referenceType); 87 | } catch (RouteNotFoundException $exception) { 88 | return $this->router->generate($name, $parameters, $referenceType); 89 | } 90 | } 91 | 92 | /** 93 | * Tries to match a URL path with a set of routes. 94 | * 95 | * If the matcher can not find information, it must throw one of the exceptions documented 96 | * below. 97 | * 98 | * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) 99 | * 100 | * @return array An array of parameters 101 | * 102 | * @throws NoConfigurationException If no routing configuration could be found 103 | * @throws ResourceNotFoundException If the resource could not be found 104 | * @throws MethodNotAllowedException If the resource was found but the request method is not allowed 105 | */ 106 | public function match($pathinfo) 107 | { 108 | return $this->router->match($pathinfo); 109 | } 110 | 111 | /** 112 | * Warms up the cache. 113 | * 114 | * @param string $cacheDir The cache directory 115 | */ 116 | public function warmUp($cacheDir) 117 | { 118 | if ($this->router instanceof WarmableInterface) { 119 | $this->router->warmUp($cacheDir); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/Routing/Loader/AnnotatedI18nRouteLoader.php: -------------------------------------------------------------------------------- 1 | isAbstract()) { 37 | throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName())); 38 | } 39 | 40 | $globals = $this->getGlobals($class); 41 | 42 | $collection = new RouteCollection(); 43 | $collection->addResource(new FileResource($class->getFileName())); 44 | 45 | foreach ($class->getMethods() as $method) { 46 | $this->defaultRouteIndex = 0; 47 | foreach ($this->reader->getMethodAnnotations($method) as $annotation) { 48 | if ($annotation instanceof SymfonyRoute) { 49 | $this->addRoute($collection, $annotation, $globals, $class, $method); 50 | } 51 | } 52 | } 53 | 54 | if (0 === $collection->count() && $class->hasMethod('__invoke') && $annotation = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { 55 | $globals['path'] = ''; 56 | $globals['name'] = ''; 57 | $globals['locales'] = []; 58 | $this->addRoute($collection, $annotation, $globals, $class, $class->getMethod('__invoke')); 59 | } 60 | 61 | return $collection; 62 | } 63 | 64 | /** 65 | * @param RouteCollection $collection 66 | * @param SymfonyRoute $annotation 67 | * @param array $globals 68 | * @param ReflectionClass $class 69 | * @param ReflectionMethod $method 70 | */ 71 | protected function addRoute(RouteCollection $collection, $annotation, $globals, ReflectionClass $class, ReflectionMethod $method) 72 | { 73 | $name = $annotation->getName(); 74 | 75 | if (null === $name) { 76 | $name = $this->getDefaultRouteName($class, $method); 77 | } 78 | 79 | $name = $globals['name_prefix'].$name; 80 | $defaults = array_replace($globals['defaults'], $annotation->getDefaults()); 81 | 82 | foreach ($method->getParameters() as $param) { 83 | if ( ! isset($defaults[$param->name]) && $param->isDefaultValueAvailable()) { 84 | $defaults[$param->name] = $param->getDefaultValue(); 85 | } 86 | } 87 | 88 | $requirements = array_replace($globals['requirements'], $annotation->getRequirements()); 89 | $options = array_replace($globals['options'], $annotation->getOptions()); 90 | $schemes = array_merge($globals['schemes'], $annotation->getSchemes()); 91 | $methods = array_merge($globals['methods'], $annotation->getMethods()); 92 | $host = $annotation->getHost() ?: $globals['host']; 93 | $condition = $annotation->getCondition() ?: $globals['condition']; 94 | $path = $annotation->getPath(); 95 | $locales = $annotation instanceof I18nRoute ? $annotation->getLocales() : []; 96 | 97 | $hasLocalizedPrefix = empty($globals['locales']) === false; 98 | $hasPrefix = $hasLocalizedPrefix || empty($globals['path']) === false; 99 | $isLocalized = ! empty($locales); 100 | $hasPathOrLocales = empty($path) === false || $isLocalized; 101 | 102 | if ($hasPrefix === false && $hasPathOrLocales === false) { 103 | throw MissingRoutePath::forAnnotation("{$class->name}::{$method->name}"); 104 | } 105 | 106 | if ( ! $hasPathOrLocales) { 107 | if ($hasLocalizedPrefix) { 108 | foreach ($globals['locales'] as $locale => $localePath) { 109 | $routeName = "{$name}.{$locale}"; 110 | $route = $this->createRoute($localePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 111 | $this->configureRoute($route, $class, $method, $annotation); 112 | $route->setDefault('_locale', $locale); 113 | $collection->add($routeName, $route); 114 | } 115 | } else { 116 | $route = $this->createRoute($globals['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 117 | $this->configureRoute($route, $class, $method, $annotation); 118 | $collection->add($name, $route); 119 | } 120 | } elseif ( ! $hasPrefix) { 121 | if ($isLocalized) { 122 | foreach ($locales as $locale => $localePath) { 123 | $routeName = "{$name}.{$locale}"; 124 | $route = $this->createRoute($localePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 125 | $this->configureRoute($route, $class, $method, $annotation); 126 | $route->setDefault('_locale', $locale); 127 | $collection->add($routeName, $route); 128 | } 129 | } else { 130 | $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 131 | $this->configureRoute($route, $class, $method, $annotation); 132 | $collection->add($name, $route); 133 | } 134 | } else { 135 | if ($hasLocalizedPrefix) { 136 | if ($isLocalized) { 137 | $missing = array_diff(array_keys($globals['locales']), array_keys($locales)); 138 | 139 | if ( ! empty($missing)) { 140 | throw MissingRouteLocale::forClass($class, $method, join(' and ', $missing)); 141 | } 142 | 143 | foreach ($locales as $locale => $localePath) { 144 | if ( ! isset($globals['locales'][$locale])) { 145 | throw MissingRouteLocale::forClass($class, $method, $locale); 146 | } 147 | 148 | $routePath = $globals['locales'][$locale] . $localePath; 149 | $routeName = "{$name}.{$locale}"; 150 | $route = $this->createRoute($routePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 151 | $this->configureRoute($route, $class, $method, $annotation); 152 | $route->setDefault('_locale', $locale); 153 | $collection->add($routeName, $route); 154 | } 155 | } else { 156 | foreach ($globals['locales'] as $locale => $localePrefix) { 157 | $routeName = "{$name}.{$locale}"; 158 | $routePath = $localePrefix . $path; 159 | $route = $this->createRoute($routePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 160 | $this->configureRoute($route, $class, $method, $annotation); 161 | $route->setDefault('_locale', $locale); 162 | $collection->add($routeName, $route); 163 | } 164 | } 165 | } else { 166 | if ($isLocalized) { 167 | foreach ($locales as $locale => $localePath) { 168 | $routePath = $globals['path'] . $localePath; 169 | $routeName = "{$name}.{$locale}"; 170 | $route = $this->createRoute($routePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 171 | $this->configureRoute($route, $class, $method, $annotation); 172 | $route->setDefault('_locale', $locale); 173 | $collection->add($routeName, $route); 174 | } 175 | } else { 176 | $routePath = $globals['path'] . $path; 177 | $route = $this->createRoute($routePath, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 178 | $this->configureRoute($route, $class, $method, $annotation); 179 | $collection->add($name, $route); 180 | } 181 | } 182 | } 183 | } 184 | 185 | /** 186 | * @inheritdoc 187 | */ 188 | protected function getGlobals(ReflectionClass $class) 189 | { 190 | $globals = [ 191 | 'path' => '', 192 | 'locales' => [], 193 | 'requirements' => [], 194 | 'options' => [], 195 | 'defaults' => [], 196 | 'schemes' => [], 197 | 'methods' => [], 198 | 'host' => '', 199 | 'condition' => '', 200 | 'name_prefix' => '', 201 | ]; 202 | 203 | $annotation = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass); 204 | 205 | if ($annotation instanceof SymfonyRoute === false) { 206 | return $globals; 207 | } 208 | if ($annotation instanceof I18nRoute) { 209 | $globals['locales'] = $annotation->getLocales(); 210 | } 211 | if (null !== $annotation->getPath()) { 212 | $globals['path'] = $annotation->getPath(); 213 | } 214 | if (null !== $annotation->getRequirements()) { 215 | $globals['requirements'] = $annotation->getRequirements(); 216 | } 217 | if (null !== $annotation->getOptions()) { 218 | $globals['options'] = $annotation->getOptions(); 219 | } 220 | if (null !== $annotation->getDefaults()) { 221 | $globals['defaults'] = $annotation->getDefaults(); 222 | } 223 | if (null !== $annotation->getSchemes()) { 224 | $globals['schemes'] = $annotation->getSchemes(); 225 | } 226 | if (null !== $annotation->getMethods()) { 227 | $globals['methods'] = $annotation->getMethods(); 228 | } 229 | if (null !== $annotation->getHost()) { 230 | $globals['host'] = $annotation->getHost(); 231 | } 232 | if (null !== $annotation->getCondition()) { 233 | $globals['condition'] = $annotation->getCondition(); 234 | } 235 | 236 | return $globals; 237 | } 238 | 239 | protected function configureRoute(Route $route, ReflectionClass $class, ReflectionMethod $method, $annot) 240 | { 241 | $route->setDefault('_controller', $class->name . '::' . $method->getName()); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Routing/Loader/MissingRouteLocale.php: -------------------------------------------------------------------------------- 1 | name} while it is/are defined on its action {$method->name}."); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Routing/Loader/MissingRoutePath.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Tobias Schultze 20 | */ 21 | class YamlFileLoader extends FileLoader 22 | { 23 | private $yamlParser; 24 | 25 | private static $availableKeys = [ 26 | 'locales', 'resource', 'type', 'prefix', 'path', 27 | 'host', 'schemes', 'methods', 'defaults', 'requirements', 28 | 'options', 'condition', 'controller', 29 | ]; 30 | 31 | /** 32 | * Loads a Yaml file. 33 | * 34 | * @param string $file A Yaml file path 35 | * @param string|null $type The resource type 36 | * 37 | * @return RouteCollection A RouteCollection instance 38 | * 39 | * @throws InvalidArgumentException When a route can't be parsed because YAML is invalid 40 | */ 41 | public function load($file, $type = null) 42 | { 43 | $path = $this->locator->locate($file, null, true); 44 | 45 | if ( ! stream_is_local($path)) { 46 | throw new InvalidArgumentException(sprintf('This is not a local file "%s".', $path)); 47 | } 48 | 49 | if ( ! file_exists($path)) { 50 | throw new InvalidArgumentException(sprintf('File "%s" not found.', $path)); 51 | } 52 | 53 | if (null === $this->yamlParser) { 54 | $this->yamlParser = new YamlParser(); 55 | } 56 | 57 | try { 58 | $parsedConfig = $this->yamlParser->parseFile($path); 59 | } catch (ParseException $e) { 60 | throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); 61 | } 62 | 63 | $collection = new RouteCollection(); 64 | $collection->addResource(new FileResource($path)); 65 | 66 | // empty file 67 | if (null === $parsedConfig) { 68 | return $collection; 69 | } 70 | 71 | // not an array 72 | if ( ! is_array($parsedConfig)) { 73 | throw new InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path)); 74 | } 75 | 76 | foreach ($parsedConfig as $name => $config) { 77 | $this->validate($config, $name, $path); 78 | 79 | if (isset($config['resource'])) { 80 | $this->parseImport($collection, $config, $path, $file); 81 | } else { 82 | $this->parseRoute($collection, $name, $config, $path); 83 | } 84 | } 85 | 86 | return $collection; 87 | } 88 | 89 | public function supports($resource, $type = null) 90 | { 91 | return $type === 'i18n_routes' 92 | && is_string($resource) 93 | && in_array(pathinfo($resource, PATHINFO_EXTENSION), ['yml', 'yaml'], true); 94 | } 95 | 96 | /** 97 | * Parses a route and adds it to the RouteCollection. 98 | * 99 | * @param RouteCollection $collection A RouteCollection instance 100 | * @param string $name Route name 101 | * @param array $config Route definition 102 | * @param string $path Full path of the YAML file being processed 103 | */ 104 | protected function parseRoute(RouteCollection $collection, $name, array $config, $path) 105 | { 106 | $defaults = isset($config['defaults']) ? $config['defaults'] : []; 107 | $requirements = isset($config['requirements']) ? $config['requirements'] : []; 108 | $options = isset($config['options']) ? $config['options'] : []; 109 | $host = isset($config['host']) ? $config['host'] : ''; 110 | $schemes = isset($config['schemes']) ? $config['schemes'] : []; 111 | $methods = isset($config['methods']) ? $config['methods'] : []; 112 | $condition = isset($config['condition']) ? $config['condition'] : null; 113 | 114 | if (isset($config['controller'])) { 115 | $defaults['_controller'] = $config['controller']; 116 | } 117 | 118 | if (isset($config['locales'])) { 119 | $routes = $this->generateLocaleRoutes( 120 | $name, 121 | $config['locales'], 122 | new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition) 123 | ); 124 | foreach ($routes as $routeName => $route) { 125 | $collection->add($routeName, $route); 126 | } 127 | } else { 128 | $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 129 | $collection->add($name, $route); 130 | } 131 | } 132 | 133 | protected function parseImport(RouteCollection $collection, array $config, $path, $file) 134 | { 135 | $type = isset($config['type']) ? $config['type'] : 'i18n_routes'; 136 | $prefix = isset($config['prefix']) ? $config['prefix'] : ''; 137 | $defaults = isset($config['defaults']) ? $config['defaults'] : []; 138 | $requirements = isset($config['requirements']) ? $config['requirements'] : []; 139 | $options = isset($config['options']) ? $config['options'] : []; 140 | $host = isset($config['host']) ? $config['host'] : null; 141 | $condition = isset($config['condition']) ? $config['condition'] : null; 142 | $schemes = isset($config['schemes']) ? $config['schemes'] : null; 143 | $methods = isset($config['methods']) ? $config['methods'] : null; 144 | 145 | if (isset($config['controller'])) { 146 | $defaults['_controller'] = $config['controller']; 147 | } 148 | 149 | $this->setCurrentDir(dirname($path)); 150 | 151 | /* @var $subCollection RouteCollection */ 152 | $subCollection = $this->import($config['resource'], $type, false, $file); 153 | $prefixIsLocalized = is_array($prefix); 154 | 155 | 156 | foreach ($subCollection->all() as $routeName => $route) { 157 | $routeLocale = $route->getDefault('_locale'); 158 | if ($prefixIsLocalized && null === $routeLocale) { 159 | throw new InvalidArgumentException("Route {$routeName} doesn't have a locale."); 160 | } 161 | if ($prefixIsLocalized && false === isset($prefix[$routeLocale])) { 162 | throw new InvalidArgumentException("Route {$routeName} with locale {$routeLocale} does not have a prefix defined in {$file}"); 163 | } 164 | 165 | $localePrefix = is_array($prefix) ? $prefix[$routeLocale] : $prefix; 166 | $route->setPath($localePrefix . $route->getPath()); 167 | } 168 | 169 | if (null !== $host) { 170 | $subCollection->setHost($host); 171 | } 172 | if (null !== $condition) { 173 | $subCollection->setCondition($condition); 174 | } 175 | if (null !== $schemes) { 176 | $subCollection->setSchemes($schemes); 177 | } 178 | if (null !== $methods) { 179 | $subCollection->setMethods($methods); 180 | } 181 | $subCollection->addDefaults($defaults); 182 | $subCollection->addRequirements($requirements); 183 | $subCollection->addOptions($options); 184 | 185 | $collection->addCollection($subCollection); 186 | } 187 | 188 | /** 189 | * Validates the route configuration. 190 | * 191 | * @param array $config A resource config 192 | * @param string $name The config key 193 | * @param string $path The loaded file path 194 | * 195 | * @throws InvalidArgumentException If one of the provided config keys is not supported, 196 | * something is missing or the combination is nonsense 197 | */ 198 | protected function validate($config, $name, $path) 199 | { 200 | if ( ! is_array($config)) { 201 | throw new InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); 202 | } 203 | if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) { 204 | throw new InvalidArgumentException(sprintf( 205 | 'The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', 206 | $path, $name, implode('", "', $extraKeys), implode('", "', self::$availableKeys) 207 | )); 208 | } 209 | if (isset($config['resource']) && (isset($config['path']) || isset($config['locales']))) { 210 | throw new InvalidArgumentException(sprintf( 211 | 'The routing file "%s" must not specify both the "resource" key and the "path" or "locales" key for "%s". Choose between an import and a route definition.', 212 | $path, $name 213 | )); 214 | } 215 | if ( ! isset($config['resource']) && isset($config['type'])) { 216 | throw new InvalidArgumentException(sprintf( 217 | 'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', 218 | $name, $path 219 | )); 220 | } 221 | if ( ! isset($config['resource']) && ! isset($config['path']) && ! isset($config['locales'])) { 222 | throw new InvalidArgumentException(sprintf( 223 | 'You must define a "path" or "locales" for the route "%s" in file "%s".', 224 | $name, $path 225 | )); 226 | } 227 | if (isset($config['controller']) && isset($config['defaults']['_controller'])) { 228 | throw new InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name)); 229 | } 230 | } 231 | 232 | private function generateLocaleRoutes(string $name, array $locales, Route $route): Generator 233 | { 234 | foreach ($locales as $locale => $path) { 235 | $localeRoute = clone $route; 236 | $localeRoute->setPath($path); 237 | $localeRoute->setDefault('_locale', $locale); 238 | yield "{$name}.{$locale}" => $localeRoute; 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /tests/AnnotatedI18nRouteLoaderTest.php: -------------------------------------------------------------------------------- 1 | loader = new AnnotatedI18nRouteLoader(new AnnotationReader()); 40 | AnnotationRegistry::registerLoader('class_exists'); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function simple_path_routes() 47 | { 48 | $routes = $this->loader->load(ActionPathController::class); 49 | $this->assertCount(1, $routes); 50 | $this->assertEquals('/path', $routes->get('action')->getPath()); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function invokable_controller_loading() 57 | { 58 | $routes = $this->loader->load(InvokableController::class); 59 | $this->assertCount(1, $routes); 60 | $this->assertEquals('/here', $routes->get('lol')->getPath()); 61 | } 62 | 63 | /** 64 | * @test 65 | */ 66 | public function invokable_localized_controller_loading() 67 | { 68 | $routes = $this->loader->load(InvokableLocalizedController::class); 69 | $this->assertCount(2, $routes); 70 | $this->assertEquals('/here', $routes->get('action.en')->getPath()); 71 | $this->assertEquals('/hier', $routes->get('action.nl')->getPath()); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function localized_path_routes() 78 | { 79 | $routes = $this->loader->load(LocalizedActionPathController::class); 80 | $this->assertCount(2, $routes); 81 | $this->assertEquals('/path', $routes->get('action.en')->getPath()); 82 | $this->assertEquals('/pad', $routes->get('action.nl')->getPath()); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function default_values_for_methods() 89 | { 90 | $routes = $this->loader->load(DefaultValueController::class); 91 | $this->assertCount(1, $routes); 92 | $this->assertEquals('/path', $routes->get('action')->getPath()); 93 | $this->assertEquals('value', $routes->get('action')->getDefault('default')); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function method_action_controllers() 100 | { 101 | $routes = $this->loader->load(MethodActionControllers::class); 102 | $this->assertCount(2, $routes); 103 | $this->assertEquals('/the/path', $routes->get('put')->getPath()); 104 | $this->assertEquals('/the/path', $routes->get('post')->getPath()); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function localized_method_action_controllers() 111 | { 112 | $routes = $this->loader->load(LocalizedMethodActionControllers::class); 113 | $this->assertCount(4, $routes); 114 | $this->assertEquals('/the/path', $routes->get('put.en')->getPath()); 115 | $this->assertEquals('/the/path', $routes->get('post.en')->getPath()); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function route_with_path_with_prefix() 122 | { 123 | $routes = $this->loader->load(PrefixedActionPathController::class); 124 | $this->assertCount(1, $routes); 125 | $route = $routes->get('action'); 126 | $this->assertEquals('/prefix/path', $route->getPath()); 127 | $this->assertEquals('lol=fun', $route->getCondition()); 128 | $this->assertEquals('frankdejonge.nl', $route->getHost()); 129 | } 130 | 131 | /** 132 | * @test 133 | */ 134 | public function localized_route_with_path_with_prefix() 135 | { 136 | $routes = $this->loader->load(PrefixedActionLocalizedRouteController::class); 137 | $this->assertCount(2, $routes); 138 | $this->assertEquals('/prefix/path', $routes->get('action.en')->getPath()); 139 | $this->assertEquals('/prefix/pad', $routes->get('action.nl')->getPath()); 140 | } 141 | 142 | /** 143 | * @test 144 | */ 145 | public function localized_prefix_localized_route() 146 | { 147 | $routes = $this->loader->load(LocalizedPrefixLocalizedActionController::class); 148 | $this->assertCount(2, $routes); 149 | $this->assertEquals('/nl/actie', $routes->get('action.nl')->getPath()); 150 | $this->assertEquals('/en/action', $routes->get('action.en')->getPath()); 151 | } 152 | 153 | /** 154 | * @test 155 | */ 156 | public function missing_a_prefix_locale() 157 | { 158 | $this->expectException(MissingRouteLocale::class); 159 | $this->loader->load(LocalizedPrefixMissingLocaleActionController::class); 160 | } 161 | 162 | /** 163 | * @test 164 | */ 165 | public function missing_a_route_locale() 166 | { 167 | $this->expectException(MissingRouteLocale::class); 168 | $this->loader->load(LocalizedPrefixMissingRouteLocaleActionController::class); 169 | } 170 | 171 | /** 172 | * @test 173 | */ 174 | public function missing_a_route_name() 175 | { 176 | $routes = $this->loader->load(MissingRouteNameController::class)->all(); 177 | $this->assertCount(1, $routes); 178 | $this->assertEquals('/path', reset($routes)->getPath()); 179 | } 180 | 181 | /** 182 | * @test 183 | */ 184 | public function nothing_but_a_name() 185 | { 186 | $this->expectException(MissingRoutePath::class); 187 | $this->loader->load(NothingButNameController::class); 188 | } 189 | 190 | /** 191 | * @test 192 | */ 193 | public function non_existing_class_loading() 194 | { 195 | $this->expectException(LogicException::class); 196 | $this->loader->load('ClassThatDoesNotExist'); 197 | } 198 | 199 | /** 200 | * @test 201 | */ 202 | public function loading_an_abstract_class() 203 | { 204 | $this->expectException(LogicException::class); 205 | $this->loader->load(AbstractClassController::class); 206 | } 207 | 208 | /** 209 | * @test 210 | */ 211 | public function localized_prefix_without_route_locale() 212 | { 213 | $routes = $this->loader->load(LocalizedPrefixWithRouteWithoutLocale::class); 214 | $this->assertCount(2, $routes); 215 | $this->assertEquals('/en/{param}', $routes->get('action.en')->getPath()); 216 | $this->assertEquals('/nl/{param}', $routes->get('action.nl')->getPath()); 217 | } 218 | 219 | /** 220 | * @test 221 | */ 222 | public function loading_route_with_prefix() 223 | { 224 | $routes = $this->loader->load(SymfonyRouteWithPrefixController::class); 225 | $this->assertCount(1, $routes); 226 | $this->assertEquals('/prefix/path', $routes->get('action')->getPath()); 227 | } 228 | } -------------------------------------------------------------------------------- /tests/AnnotationFixtures/AbstractClassController.php: -------------------------------------------------------------------------------- 1 | expectException(BadMethodCallException::class); 14 | new I18nRoute(['invalid' => 'property']); 15 | } 16 | 17 | /** 18 | * @test 19 | * @dataProvider validPropertiesProvider 20 | */ 21 | public function passing_valid_properties($property, $value) 22 | { 23 | $route = new I18nRoute([$property => $value]); 24 | $this->assertEquals($value, $route->{"get" . ucfirst($property)}()); 25 | } 26 | 27 | public function validPropertiesProvider() 28 | { 29 | return [ 30 | ['path', 'value'], 31 | ['locales', ['nl', 'es']], 32 | ['name', 'value'], 33 | ['requirements', ['segment' => '.*']], 34 | ['options', ['option' => true]], 35 | ['defaults', ['default' => 'value']], 36 | ['host', 'localhost'], 37 | ['methods', ['POST', 'PUT']], 38 | ['schemes', ['ftp', 'https']], 39 | ['condition', 'something=valid'], 40 | ]; 41 | } 42 | 43 | /** 44 | * @test 45 | * @dataProvider validValues 46 | */ 47 | public function passing_valid_values($getter, $value) 48 | { 49 | $route = new I18nRoute(['value' => $value]); 50 | $this->assertEquals($value, $route->{$getter}()); 51 | } 52 | 53 | public function validValues() 54 | { 55 | return [ 56 | ['getPath', '/nl'], 57 | ['getLocales', ['nl' => '/nl']], 58 | ]; 59 | } 60 | } -------------------------------------------------------------------------------- /tests/I18nRoutingBundleTest.php: -------------------------------------------------------------------------------- 1 | bootKernel(); 23 | $container = $this->getContainer(); 24 | $router = $container->get('router'); 25 | $this->assertInstanceOf(I18nRouter::class, $router); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/I18nRoutingExtensionTest.php: -------------------------------------------------------------------------------- 1 | 'en']; 17 | } 18 | 19 | /** 20 | * @test 21 | */ 22 | public function it_registers_a_router() 23 | { 24 | $this->load(); 25 | $this->assertContainerBuilderHasService('frankdejonge_i18n_routing.router'); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function loading_annotation_services() 32 | { 33 | $this->load(['use_annotations' => true]); 34 | $this->assertContainerBuilderHasService('frankdejonge_i18n_routing.annotation.class_loader', AnnotatedI18nRouteLoader::class); 35 | $this->assertContainerBuilderHasService('frankdejonge_i18n_routing.annotation.file_loader', AnnotationFileLoader::class); 36 | $this->assertContainerBuilderHasService('frankdejonge_i18n_routing.annotation.directory_loader', AnnotationDirectoryLoader::class); 37 | } 38 | 39 | /** 40 | * Return an array of container extensions you need to be registered for each test (usually just the container 41 | * extension you are testing. 42 | * 43 | * @return ExtensionInterface[] 44 | */ 45 | protected function getContainerExtensions() 46 | { 47 | return [(new I18nRoutingBundle())->getContainerExtension()]; 48 | } 49 | } -------------------------------------------------------------------------------- /tests/I18nRoutingRouterTest.php: -------------------------------------------------------------------------------- 1 | add('home', new Route('/')); 20 | $internalRouter = new RouterStub($routes); 21 | $this->assertFalse($internalRouter->warmed); 22 | $router = new I18nRouter($internalRouter, 'en'); 23 | 24 | $this->assertEquals($routes, $router->getRouteCollection()); 25 | 26 | $router->warmUp('string'); 27 | $this->assertTrue($internalRouter->warmed); 28 | 29 | $newContext = new RequestContext('/new-context/'); 30 | $this->assertNull($router->getContext()); 31 | $router->setContext($newContext); 32 | $this->assertEquals($newContext, $router->getContext()); 33 | 34 | $match = $router->match('/'); 35 | $this->assertEquals(['_route' => 'home'], $match); 36 | 37 | $this->assertEquals('http://not_i18n/', $router->generate('not_i18n')); 38 | $this->assertEquals('http://is_i18n.en/', $router->generate('is_i18n')); 39 | } 40 | } -------------------------------------------------------------------------------- /tests/RouterStub.php: -------------------------------------------------------------------------------- 1 | routes = $routes ?: new RouteCollection(); 35 | } 36 | 37 | public function setContext(RequestContext $context) 38 | { 39 | $this->context = $context; 40 | } 41 | 42 | public function getContext() 43 | { 44 | return $this->context; 45 | } 46 | 47 | public function getRouteCollection() 48 | { 49 | return $this->routes; 50 | } 51 | 52 | /** 53 | * Generates a URL or path for a specific route based on the given parameters. 54 | * 55 | * Parameters that reference placeholders in the route pattern will substitute them in the 56 | * path or host. Extra params are added as query string to the URL. 57 | * 58 | * When the passed reference type cannot be generated for the route because it requires a different 59 | * host or scheme than the current one, the method will return a more comprehensive reference 60 | * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH 61 | * but the route requires the https scheme whereas the current scheme is http, it will instead return an 62 | * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches 63 | * the route in any case. 64 | * 65 | * If there is no route with the given name, the generator must throw the RouteNotFoundException. 66 | * 67 | * The special parameter _fragment will be used as the document fragment suffixed to the final URL. 68 | * 69 | * @param string $name The name of the route 70 | * @param mixed $parameters An array of parameters 71 | * @param int $referenceType The type of reference to be generated (one of the constants) 72 | * 73 | * @return string The generated URL 74 | * 75 | * @throws RouteNotFoundException If the named route doesn't exist 76 | * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route 77 | * @throws InvalidParameterException When a parameter value for a placeholder is not correct because 78 | * it does not match the requirement 79 | */ 80 | public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) 81 | { 82 | if ($name === 'not_i18n.en') { 83 | throw new RouteNotFoundException(); 84 | } 85 | 86 | return 'http://'. $name . '/'; 87 | } 88 | 89 | /** 90 | * Tries to match a URL path with a set of routes. 91 | * 92 | * If the matcher can not find information, it must throw one of the exceptions documented 93 | * below. 94 | * 95 | * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) 96 | * 97 | * @return array An array of parameters 98 | * 99 | * @throws NoConfigurationException If no routing configuration could be found 100 | * @throws ResourceNotFoundException If the resource could not be found 101 | * @throws MethodNotAllowedException If the resource was found but the request method is not allowed 102 | */ 103 | public function match($pathinfo) 104 | { 105 | return (new UrlMatcher($this->routes, new RequestContext('/')))->match($pathinfo); 106 | } 107 | 108 | /** 109 | * Warms up the cache. 110 | * 111 | * @param string $cacheDir The cache directory 112 | */ 113 | public function warmUp($cacheDir) 114 | { 115 | $this->warmed = true; 116 | } 117 | } -------------------------------------------------------------------------------- /tests/YamlFileLoaderTest.php: -------------------------------------------------------------------------------- 1 | locator = new FileLocator(__DIR__.'/fixtures/'); 28 | $this->loader = new YamlFileLoader($this->locator); 29 | } 30 | 31 | public function setupStubbedLoader() 32 | { 33 | $this->locator = new FileLocatorStub(); 34 | $this->loader = new YamlFileLoader($this->locator); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function test_it_accepts_yaml_files() 41 | { 42 | $this->assertTrue($this->loader->supports('something.yaml', 'i18n_routes')); 43 | $this->assertTrue($this->loader->supports('something.yml', 'i18n_routes')); 44 | $this->assertFalse($this->loader->supports('something.yaml', 'routes')); 45 | $this->assertFalse($this->loader->supports('something.xml')); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function loading_an_empty_file() 52 | { 53 | $routes = $this->loader->load('empty.yml'); 54 | $this->assertEmpty($routes->all()); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function remote_sources_are_not_accepted() 61 | { 62 | $this->setupStubbedLoader(); 63 | $this->expectException(InvalidArgumentException::class); 64 | $this->loader->load('http://remote.com/here.yml'); 65 | } 66 | 67 | /** 68 | * @test 69 | */ 70 | public function loading_non_existing_files() 71 | { 72 | $this->setupStubbedLoader(); 73 | $this->expectException(InvalidArgumentException::class); 74 | $this->loader->load('non-existing.yml'); 75 | } 76 | 77 | /** 78 | * @test 79 | */ 80 | public function loading_invalid_yaml() 81 | { 82 | $this->expectException(InvalidArgumentException::class); 83 | $this->loader->load('invalid.yml'); 84 | } 85 | 86 | /** 87 | * @test 88 | */ 89 | public function loading_not_an_array() 90 | { 91 | $this->expectException(InvalidArgumentException::class); 92 | $this->loader->load('not-an-array.yml'); 93 | } 94 | 95 | /** 96 | * @test 97 | */ 98 | public function loading_a_localized_route() 99 | { 100 | $routes = $this->loader->load('localized-route.yml'); 101 | 102 | $this->assertCount(3, $routes); 103 | } 104 | 105 | /** 106 | * @test 107 | */ 108 | public function importing_routes_from_a_definition() 109 | { 110 | $routes = $this->loader->load('importing-localized-route.yml'); 111 | 112 | $this->assertCount(3, $routes); 113 | $this->assertEquals('/nl', $routes->get('home.nl')->getPath()); 114 | $this->assertEquals('/en', $routes->get('home.en')->getPath()); 115 | $this->assertEquals('/here', $routes->get('not_localized')->getPath()); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function importing_routes_with_locales() 122 | { 123 | $routes = $this->loader->load('importer-with-locale.yml'); 124 | 125 | $this->assertCount(2, $routes); 126 | $this->assertEquals('/nl/voorbeeld', $routes->get('imported.nl')->getPath()); 127 | $this->assertEquals('/en/example', $routes->get('imported.en')->getPath()); 128 | } 129 | 130 | /** 131 | * @test 132 | */ 133 | public function importing_routes_from_a_definition_missing_a_locale_prefix() 134 | { 135 | $this->expectException(InvalidArgumentException::class); 136 | $this->loader->load('missing-locale-in-importer.yml'); 137 | } 138 | 139 | /** 140 | * @test 141 | */ 142 | public function importing_not_localized_routes_from_a_localized_import() 143 | { 144 | $this->expectException(InvalidArgumentException::class); 145 | $this->loader->load('importing-not-localized-with-localized-prefix.yml'); 146 | } 147 | 148 | /** 149 | * @test 150 | */ 151 | public function importing_a_route_that_is_not_an_array() 152 | { 153 | $this->expectException(InvalidArgumentException::class); 154 | $this->loader->load('route-is-not-an-array.yml'); 155 | } 156 | 157 | /** 158 | * @test 159 | */ 160 | public function importing_a_route_with_too_many_properties() 161 | { 162 | $this->expectException(InvalidArgumentException::class); 163 | $this->loader->load('route-has-too-many-properties.yml'); 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function importing_a_route_without_a_path_or_locales() 170 | { 171 | $this->expectException(InvalidArgumentException::class); 172 | $this->loader->load('route-without-path-or-locales.yml'); 173 | } 174 | 175 | /** 176 | * @test 177 | */ 178 | public function importing_a_route_with_a_resource() 179 | { 180 | $this->expectException(InvalidArgumentException::class); 181 | $this->loader->load('route-with-resource.yml'); 182 | } 183 | 184 | /** 185 | * @test 186 | */ 187 | public function importing_a_route_with_a_type() 188 | { 189 | $this->expectException(InvalidArgumentException::class); 190 | $this->loader->load('route-with-type.yml'); 191 | } 192 | 193 | /** 194 | * @test 195 | */ 196 | public function importing_a_route_without_a_controller() 197 | { 198 | $this->expectException(InvalidArgumentException::class); 199 | $this->loader->load('route-with-2-controllers.yml'); 200 | } 201 | 202 | /** 203 | * @test 204 | */ 205 | public function importing_with_a_controller_default() 206 | { 207 | $routes = $this->loader->load('importer-with-controller-default.yml'); 208 | $this->assertCount(3, $routes); 209 | $controller = $routes->get('home.en')->getDefault('_controller'); 210 | $this->assertEquals('DefaultController::defaultAction', $controller); 211 | } 212 | 213 | /** 214 | * @test 215 | */ 216 | public function importing_with_a_full_definition() 217 | { 218 | $routes = $this->loader->load('importer-with-all-options.yml'); 219 | $this->assertCount(3, $routes); 220 | $route = $routes->get('home.en'); 221 | 222 | $this->assertEquals(['POST', 'GET'], $route->getMethods()); 223 | $this->assertEquals(['https', 'http'], $route->getSchemes()); 224 | $this->assertEquals("context.getMethod() in ['GET', 'HEAD']", $route->getCondition()); 225 | } 226 | } -------------------------------------------------------------------------------- /tests/fixtures/empty.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /tests/fixtures/imported-with-locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | imported: 3 | controller: ImportedController::someAction 4 | locales: 5 | nl: /voorbeeld 6 | en: /example -------------------------------------------------------------------------------- /tests/fixtures/importer-with-all-options.yml: -------------------------------------------------------------------------------- 1 | --- 2 | i_need: 3 | resource: ./localized-route.yml 4 | type: i18n_routes 5 | host: localhost 6 | methods: [POST, GET] 7 | schemes: [https,http] 8 | condition: "context.getMethod() in ['GET', 'HEAD']" -------------------------------------------------------------------------------- /tests/fixtures/importer-with-controller-default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | i_need: 3 | controller: DefaultController::defaultAction 4 | resource: ./localized-route.yml 5 | type: i18n_routes -------------------------------------------------------------------------------- /tests/fixtures/importer-with-locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | i_need: 3 | resource: ./imported-with-locale.yml 4 | prefix: 5 | nl: /nl 6 | en: /en -------------------------------------------------------------------------------- /tests/fixtures/importing-localized-route.yml: -------------------------------------------------------------------------------- 1 | --- 2 | i_need: 3 | resource: ./localized-route.yml 4 | type: i18n_routes -------------------------------------------------------------------------------- /tests/fixtures/importing-not-localized-with-localized-prefix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | import_these: 3 | resource: ./not-localized.yml 4 | type: i18n_routes 5 | prefix: 6 | nl: /nl 7 | en: /en -------------------------------------------------------------------------------- /tests/fixtures/invalid.yml: -------------------------------------------------------------------------------- 1 | opwjef: [ -------------------------------------------------------------------------------- /tests/fixtures/localized-route.yml: -------------------------------------------------------------------------------- 1 | --- 2 | home: 3 | locales: 4 | nl: /nl 5 | en: /en 6 | 7 | not_localized: 8 | controller: HomeController::otherAction 9 | path: /here -------------------------------------------------------------------------------- /tests/fixtures/missing-locale-in-importer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | importing_with_missing_prefix: 3 | resource: ./localized-route.yml 4 | type: i18n_routes 5 | prefix: 6 | nl: /prefix -------------------------------------------------------------------------------- /tests/fixtures/not-an-array.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "something" -------------------------------------------------------------------------------- /tests/fixtures/not-localized.yml: -------------------------------------------------------------------------------- 1 | --- 2 | not_localized: 3 | controller: string 4 | path: /here -------------------------------------------------------------------------------- /tests/fixtures/route-has-too-many-properties.yml: -------------------------------------------------------------------------------- 1 | --- 2 | routename: 3 | path: /haha 4 | unknown: option -------------------------------------------------------------------------------- /tests/fixtures/route-is-not-an-array.yml: -------------------------------------------------------------------------------- 1 | --- 2 | routename: 1234 -------------------------------------------------------------------------------- /tests/fixtures/route-with-2-controllers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | home: 3 | controller: haha::haha 4 | defaults: 5 | _controller: lol::lol 6 | locales: 7 | nl: /nl 8 | en: /en -------------------------------------------------------------------------------- /tests/fixtures/route-with-resource.yml: -------------------------------------------------------------------------------- 1 | --- 2 | routename: 3 | controller: HomeController::indexAction 4 | locales: 5 | nl: /nl 6 | en: /en 7 | resource: ./localized-route.yml -------------------------------------------------------------------------------- /tests/fixtures/route-with-type.yml: -------------------------------------------------------------------------------- 1 | --- 2 | routename: 3 | controller: HomeController::indexAction 4 | locales: 5 | nl: /nl 6 | en: /en 7 | type: i18n_routes -------------------------------------------------------------------------------- /tests/fixtures/route-without-path-or-locales.yml: -------------------------------------------------------------------------------- 1 | --- 2 | routename: 3 | controller: Here::here --------------------------------------------------------------------------------