├── src
├── Routing
│ ├── Exception
│ │ ├── ExceptionInterface.php
│ │ ├── MissingRouteLocaleException.php
│ │ ├── MissingLocaleException.php
│ │ └── UnknownLocaleException.php
│ ├── RouteGenerator
│ │ ├── NameInflector
│ │ │ ├── PostfixInflector.php
│ │ │ ├── RouteNameInflectorInterface.php
│ │ │ └── DefaultPostfixInflector.php
│ │ ├── RouteGeneratorInterface.php
│ │ ├── FilteredLocaleGenerator.php
│ │ ├── StrictLocaleRouteGenerator.php
│ │ └── I18nRouteGenerator.php
│ ├── Translator
│ │ ├── TranslationTranslator.php
│ │ ├── DoctrineDBAL
│ │ │ └── SchemaListener.php
│ │ ├── AttributeTranslatorInterface.php
│ │ └── DoctrineDBALTranslator.php
│ ├── Loader
│ │ ├── schema
│ │ │ └── routing
│ │ │ │ └── routing-1.0.xsd
│ │ ├── AnnotatedRouteControllerLoader.php
│ │ ├── YamlFileLoader.php
│ │ └── XmlFileLoader.php
│ ├── Annotation
│ │ └── I18nRoute.php
│ └── Router.php
├── BeSimpleI18nRoutingBundle.php
├── Resources
│ ├── meta
│ │ └── LICENSE
│ └── config
│ │ ├── annotation.xml
│ │ ├── dbal.xml
│ │ └── routing.xml
└── DependencyInjection
│ ├── Compiler
│ └── OverrideRoutingCompilerPass.php
│ ├── Configuration.php
│ └── BeSimpleI18nRoutingExtension.php
├── .php_cs
├── .editorconfig
├── CONTRIBUTORS.md
├── CHANGELOG.md
├── composer.json
├── UPGRADE-2.4.md
└── README.md
/src/Routing/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
8 | *
9 | * @api
10 | */
11 | interface ExceptionInterface
12 | {
13 | }
14 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | in(__DIR__ . '/src')
7 | ;
8 |
9 | return Symfony\CS\Config\Config::create()
10 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
11 | ->finder($finder)
12 | ;
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_size = 4
9 | indent_style = space
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/src/Routing/Exception/MissingRouteLocaleException.php:
--------------------------------------------------------------------------------
1 |
8 | *
9 | * @api
10 | */
11 | class MissingRouteLocaleException extends \InvalidArgumentException implements ExceptionInterface
12 | {
13 | }
14 |
--------------------------------------------------------------------------------
/src/Routing/Exception/MissingLocaleException.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new OverrideRoutingCompilerPass());
16 |
17 | parent::build($container);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Routing/RouteGenerator/NameInflector/DefaultPostfixInflector.php:
--------------------------------------------------------------------------------
1 | defaultLocale = $defaultLocale;
17 | }
18 |
19 | /**
20 | * @inheritdoc
21 | */
22 | public function inflect($name, $locale)
23 | {
24 | if ($this->defaultLocale === $locale) {
25 | return $name;
26 | }
27 |
28 | return $name.'.'.$locale;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Routing/RouteGenerator/RouteGeneratorInterface.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class TranslationTranslator implements AttributeTranslatorInterface
15 | {
16 | /**
17 | * @var TranslatorInterface
18 | */
19 | private $translator;
20 |
21 | public function __construct(TranslatorInterface $translator)
22 | {
23 | $this->translator = $translator;
24 | }
25 |
26 | public function reverseTranslate($route, $locale, $attribute, $originalValue)
27 | {
28 | $domain = $route . "_" . $attribute;
29 | return $this->translator->trans($originalValue, array(), $domain, $locale);
30 | }
31 |
32 | public function translate($route, $locale, $attribute, $localizedValue)
33 | {
34 | $domain = $route . "_" . $attribute;
35 | return $this->translator->trans($localizedValue, array(), $domain, $locale);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Routing/Translator/DoctrineDBAL/SchemaListener.php:
--------------------------------------------------------------------------------
1 | getSchema();
13 | $this->addRoutingTranslationsTable($schema);
14 | }
15 |
16 | public function addRoutingTranslationsTable(Schema $schema)
17 | {
18 | $table = $schema->createTable('routing_translations');
19 | $table->addColumn('id', 'integer', array('autoincrement' => true));
20 | $table->addColumn('route', 'string');
21 | $table->addColumn('locale', 'string');
22 | $table->addColumn('attribute', 'string');
23 | $table->addColumn('localized_value', 'string');
24 | $table->addColumn('original_value', 'string');
25 | $table->setPrimaryKey(array('id'));
26 | $table->addUniqueIndex(array('route', 'locale', 'attribute'));
27 | $table->addIndex(array('localized_value')); // this is much more selective than the unique index
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Resources/config/annotation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/OverrideRoutingCompilerPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('be_simple_i18n_routing.router')) {
15 | return;
16 | }
17 |
18 | if ($container->hasAlias('router')) {
19 | // router is an alias.
20 | // Register a private alias for this service to inject it as the parent
21 | $container->setAlias('be_simple_i18n_routing.router.parent', new Alias((string) $container->getAlias('router'), false));
22 | } elseif ($container->hasDefinition('router')) {
23 | // router is a definition.
24 | // Register it again as a private service to inject it as the parent
25 | $definition = $container->getDefinition('router');
26 | $definition->setPublic(false);
27 | $container->setDefinition('be_simple_i18n_routing.router.parent', $definition);
28 | } else {
29 | throw new ServiceNotFoundException('router', 'be_simple_i18n_routing.router');
30 | }
31 |
32 | $container->setAlias('router', 'be_simple_i18n_routing.router');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Routing/RouteGenerator/FilteredLocaleGenerator.php:
--------------------------------------------------------------------------------
1 | routeGenerator = $internalRouteGenerator;
23 | $this->locales = array_flip(array_values($allowedLocales));
24 | }
25 |
26 | /**
27 | * @inheritdoc
28 | */
29 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute)
30 | {
31 | return $this->routeGenerator->generateRoutes(
32 | $name,
33 | array_intersect_key($localesWithPaths, $this->locales),
34 | $baseRoute
35 | );
36 | }
37 |
38 | /**
39 | * @inheritdoc
40 | */
41 | public function generateCollection($localesWithPrefix, RouteCollection $baseCollection)
42 | {
43 | if (is_array($localesWithPrefix)) {
44 | $localesWithPrefix = array_intersect_key($localesWithPrefix, $this->locales);
45 | }
46 |
47 | return $this->routeGenerator->generateCollection($localesWithPrefix, $baseCollection);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Routing/Translator/AttributeTranslatorInterface.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | interface AttributeTranslatorInterface
26 | {
27 | /**
28 | * Translate a route attribute.
29 | *
30 | * Always returns a value, if no translation is found the original value.
31 | *
32 | * @param string $route
33 | * @param string $locale
34 | * @param string $attribute
35 | * @param string $localizedValue
36 | * @return string
37 | */
38 | public function translate($route, $locale, $attribute, $localizedValue);
39 |
40 | /**
41 | * Reverse Translate a value into its current locale.
42 | *
43 | * This feature can optionally be used when generating route urls by passing
44 | * the "translate" parameter to RouterInterface::generate()
45 | * specifying which attributes should be translated.
46 | *
47 | * @param string $route
48 | * @param string $locale
49 | * @param string $attribute
50 | * @param string $originalValue
51 | * @return string
52 | */
53 | public function reverseTranslate($route, $locale, $attribute, $originalValue);
54 | }
55 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "besimple/i18n-routing-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Full routing internationalized on your Symfony2 project ",
5 | "keywords": ["routing", "internationalisation", "Symfony2", "BeSimpleI18nRoutingBundle"],
6 | "homepage": "https://github.com/BeSimple/BeSimpleI18nRoutingBundle",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Francis Besset",
11 | "email": "francis.besset@gmail.com"
12 | },
13 | {
14 | "name": "Christophe Coevoet",
15 | "email": "stof@notk.org"
16 | },
17 | {
18 | "name": "Benjamin Eberlei",
19 | "email": "kontakt@beberlei.de"
20 | },
21 | {
22 | "name": "BeSimple Community",
23 | "homepage": "https://github.com/BeSimple/BeSimpleI18nRoutingBundle/contributors"
24 | }
25 | ],
26 | "require": {
27 | "php": ">=5.3.2",
28 |
29 | "symfony/routing": "~2.3 || ~3.0",
30 | "symfony/config": "~2.3 || ~3.0"
31 | },
32 | "require-dev": {
33 | "doctrine/dbal": "~2.2",
34 |
35 | "symfony/dependency-injection": "~2.3 || ~3.0",
36 | "symfony/http-kernel": "~2.3 || ~3.0",
37 | "symfony/translation": "~2.3 || ~3.0",
38 |
39 | "phpunit/phpunit": "~4.7@stable",
40 |
41 | "matthiasnoback/symfony-dependency-injection-test": "^0.7",
42 | "matthiasnoback/symfony-config-test": "^1.4"
43 | },
44 | "suggest": {
45 | "doctrine/dbal": "~2.2"
46 | },
47 | "autoload": {
48 | "psr-4": { "BeSimple\\I18nRoutingBundle\\": "src/" }
49 | },
50 | "autoload-dev": {
51 | "psr-4": {
52 | "BeSimple\\I18nRoutingBundle\\Tests\\": "tests/"
53 | }
54 | },
55 | "extra": {
56 | "branch-alias": {
57 | "dev-master": "2.4-dev"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Routing/Loader/schema/routing/routing-1.0.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/UPGRADE-2.4.md:
--------------------------------------------------------------------------------
1 | # UPGRADE FROM 2.3 to 2.4
2 |
3 | ## Configuration
4 |
5 | * The `locale` container parameter was used for the default locale this need to be specified from now on.
6 |
7 | After:
8 | ```YAML
9 | # app/config/config.yml
10 | be_simple_i18n_routing:
11 | locales:
12 | default_locale: "%locale%"
13 | ```
14 |
15 | ## Routing
16 |
17 | * The `I18nRouteCollectionBuilder` class has been removed in favor of `I18nRouteGenerator`.
18 |
19 | Before:
20 | ```PHP
21 | use BeSimple\I18nRoutingBundle\Routing\I18nRouteCollectionBuilder;
22 |
23 | $builder = new I18nRouteCollectionBuilder();
24 | $builder->buildCollection(
25 | 'homepage',
26 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'),
27 | array('_controller' => 'MyWebsiteBundle:Frontend:index')
28 | );
29 | ```
30 |
31 | After:
32 | ```PHP
33 | use BeSimple\I18nRoutingBundle\Routing\RouteGenerator\I18nRouteGenerator;
34 |
35 | $generator = new I18nRouteGenerator();
36 | $generator->generateRoutes(
37 | 'homepage',
38 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'),
39 | new Route('', array(
40 | '_controller' => 'MyWebsiteBundle:Frontend:index'
41 | ))
42 | );
43 | ```
44 |
45 | * The `I18nRouteCollection` class has been removed in favor of using the Symfony `RouteCollection` with the `I18nRouteGenerator`.
46 |
47 | Before:
48 | ```PHP
49 | use BeSimple\I18nRoutingBundle\Routing\I18nRouteCollection;
50 |
51 | $collection = new I18nRouteCollection();
52 | $collection->addResource(new FileResource($path));
53 | $collection->addPrefix($prefix);
54 | ```
55 |
56 | After:
57 | ```PHP
58 | use BeSimple\I18nRoutingBundle\Routing\RouteGenerator\I18nRouteGenerator;
59 |
60 | $collection = new \Symfony\Component\Routing\RouteCollection();
61 | $collection->addResource(new FileResource($path));
62 |
63 | $generator = new I18nRouteGenerator();
64 | $collection = $generator->generateCollection($prefix, $collection);
65 | ```
66 |
67 | ## Loaders
68 |
69 | * The `XmlFileLoader` and `YamlFileLoader` now required a `RouteGeneratorInterface` instance or null as a second constructor parameter instead of the now removed `BeSimple\I18nRoutingBundle\Routing\I18nRouteCollectionBuilder`.
70 |
71 | * The `XmlFileLoader` class no longer inherits from `Symfony\Component\Routing\Loader\XmlFileLoader`. You may need to change your code hints from `Symfony\Component\Routing\Loader\XmlFileLoader` to `Symfony\Component\Config\Loader\FileLoader`.
72 |
73 | * The `YamlFileLoader` class no longer inherits from `Symfony\Component\Routing\Loader\YamlFileLoader`. You may need to change your code hints from `Symfony\Component\Routing\Loader\YamlFileLoader` to `Symfony\Component\Config\Loader\FileLoader`.
74 |
--------------------------------------------------------------------------------
/src/Routing/RouteGenerator/StrictLocaleRouteGenerator.php:
--------------------------------------------------------------------------------
1 | routeGenerator = $internalRouteGenerator;
26 | $this->locales = $supportedLocales;
27 | }
28 |
29 | public function allowFallback($enabled = true)
30 | {
31 | $this->allowFallback = $enabled;
32 | }
33 |
34 | /**
35 | * Generate localized versions of the given route.
36 | *
37 | * @param $name
38 | * @param array $localesWithPaths
39 | * @param Route $baseRoute
40 | * @return RouteCollection
41 | */
42 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute)
43 | {
44 | $this->assertLocalesAreSupported(array_keys($localesWithPaths));
45 |
46 | return $this->routeGenerator->generateRoutes($name, $localesWithPaths, $baseRoute);
47 | }
48 |
49 | /**
50 | * Generate a localized version of the given route collection.
51 | *
52 | * @param array|string $localesWithPrefix
53 | * @param RouteCollection $baseCollection
54 | * @return RouteCollection
55 | */
56 | public function generateCollection($localesWithPrefix, RouteCollection $baseCollection)
57 | {
58 | if (is_array($localesWithPrefix)) {
59 | $this->assertLocalesAreSupported(array_keys($localesWithPrefix));
60 | }
61 |
62 | return $this->routeGenerator->generateCollection($localesWithPrefix, $baseCollection);
63 | }
64 |
65 | private function assertLocalesAreSupported(array $locales)
66 | {
67 | if (!$this->allowFallback) {
68 | $missingLocales = array_diff($this->locales, $locales);
69 | if (!empty($missingLocales)) {
70 | throw MissingLocaleException::shouldSupportLocale($missingLocales);
71 | }
72 | }
73 |
74 | $unknownLocales = array_diff($locales, $this->locales);
75 | if (!empty($unknownLocales)) {
76 | throw UnknownLocaleException::unexpectedLocale($unknownLocales, $this->locales);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Routing/Annotation/I18nRoute.php:
--------------------------------------------------------------------------------
1 | $value) {
38 | $method = 'set'.str_replace('_', '', $key);
39 | if (!method_exists($this, $method)) {
40 | throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, get_class($this)));
41 | }
42 | $this->$method($value);
43 | }
44 | }
45 |
46 | public function setLocales($locales)
47 | {
48 | $this->locales = $locales;
49 | }
50 |
51 | public function getLocales()
52 | {
53 | return $this->locales;
54 | }
55 |
56 | public function setHost($pattern)
57 | {
58 | $this->host = $pattern;
59 | }
60 |
61 | public function getHost()
62 | {
63 | return $this->host;
64 | }
65 |
66 | public function setName($name)
67 | {
68 | $this->name = $name;
69 | }
70 |
71 | public function getName()
72 | {
73 | return $this->name;
74 | }
75 |
76 | public function setRequirements($requirements)
77 | {
78 | if (isset($requirements['_method'])) {
79 | if (0 === count($this->methods)) {
80 | $this->methods = explode('|', $requirements['_method']);
81 | }
82 | }
83 |
84 | if (isset($requirements['_scheme'])) {
85 | if (0 === count($this->schemes)) {
86 | $this->schemes = explode('|', $requirements['_scheme']);
87 | }
88 | }
89 |
90 | $this->requirements = $requirements;
91 | }
92 |
93 | public function getRequirements()
94 | {
95 | return $this->requirements;
96 | }
97 |
98 | public function setOptions($options)
99 | {
100 | $this->options = $options;
101 | }
102 |
103 | public function getOptions()
104 | {
105 | return $this->options;
106 | }
107 |
108 | public function setDefaults($defaults)
109 | {
110 | $this->defaults = $defaults;
111 | }
112 |
113 | public function getDefaults()
114 | {
115 | return $this->defaults;
116 | }
117 |
118 | public function setSchemes($schemes)
119 | {
120 | $this->schemes = is_array($schemes) ? $schemes : array($schemes);
121 | }
122 |
123 | public function getSchemes()
124 | {
125 | return $this->schemes;
126 | }
127 |
128 | public function setMethods($methods)
129 | {
130 | $this->methods = is_array($methods) ? $methods : array($methods);
131 | }
132 |
133 | public function getMethods()
134 | {
135 | return $this->methods;
136 | }
137 |
138 | public function setCondition($condition)
139 | {
140 | $this->condition = $condition;
141 | }
142 |
143 | public function getCondition()
144 | {
145 | return $this->condition;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/Resources/config/dbal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | BeSimple\I18nRoutingBundle\Routing\Translator\DoctrineDBALTranslator
9 | BeSimple\I18nRoutingBundle\Routing\Translator\DoctrineDBAL\SchemaListener
10 | Doctrine\Common\Cache\ArrayCache
11 | Doctrine\Common\Cache\XcacheCache
12 | Doctrine\Common\Cache\ApcCache
13 | Doctrine\Common\Cache\MemcacheCache
14 | Memcache
15 | localhost
16 | 11211
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | %be_simple_i18n_routing.doctrine_dbal.connection_name%
27 |
28 |
29 |
30 |
31 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace%
32 |
33 |
34 |
35 |
36 |
37 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace%
38 |
39 |
40 |
41 |
42 |
43 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace%
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace%
53 |
54 |
55 |
56 |
57 |
58 | %be_simple_i18n_routing.doctrine_dbal.cache.memcache_host%
59 | %be_simple_i18n_routing.doctrine_dbal.cache.memcache_port%
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/Resources/config/routing.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 | BeSimple\I18nRoutingBundle\Routing\Router
11 | BeSimple\I18nRoutingBundle\Routing\Loader\XmlFileLoader
12 | BeSimple\I18nRoutingBundle\Routing\Loader\YamlFileLoader
13 | BeSimple\I18nRoutingBundle\Routing\Translator\TranslationTranslator
14 |
15 |
16 |
17 |
18 |
19 |
20 | %be_simple_i18n_routing.default_locale%
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
54 |
55 | %be_simple_i18n_routing.locales%
56 |
57 |
58 |
61 |
62 | %be_simple_i18n_routing.locales%
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
76 | %be_simple_i18n_routing.default_locale%
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/Routing/Translator/DoctrineDBALTranslator.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class DoctrineDBALTranslator implements AttributeTranslatorInterface
17 | {
18 | /**
19 | * @var Doctrine\DBAL\Connection
20 | */
21 | private $connection;
22 |
23 | /**
24 | *
25 | * @var Doctrine\Common\Cache\Cache
26 | */
27 | private $cache;
28 |
29 | /**
30 | * Prime the cache when using {@see addTranslation()} yes or no.
31 | *
32 | * @var bool
33 | */
34 | private $primeCache;
35 |
36 | public function __construct(Connection $connection, Cache $cache, $primeCache = true)
37 | {
38 | $this->connection = $connection;
39 | $this->cache = $cache;
40 | $this->primeCache = $primeCache;
41 | }
42 |
43 | /**
44 | * Translate using Doctrine DBAL and a cache layer around it.
45 | *
46 | * @param string $route
47 | * @param string $locale
48 | * @param string $attribute
49 | * @param string $value
50 | * @return string
51 | */
52 | public function translate($route, $locale, $attribute, $value)
53 | {
54 | // values can potentially be large, so we hash them and prevent collisions
55 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $value;
56 | $cacheKey = "besimplei18nroute__" . sha1($hashKey);
57 | $translatedValues = $this->cache->fetch($cacheKey);
58 | if ($translatedValues && isset($translatedValues[$hashKey])) {
59 | return $translatedValues[$hashKey];
60 | }
61 |
62 | $query = "SELECT original_value FROM routing_translations ".
63 | "WHERE route = ? AND locale = ? AND attribute = ? AND localized_value = ?";
64 | if ($translatedValue = $this->connection->fetchColumn($query, array($route, $locale, $attribute, $value))) {
65 | $value = $translatedValue;
66 | }
67 |
68 | $translatedValues[$hashKey] = $value;
69 | $this->cache->save($cacheKey, $translatedValues);
70 |
71 | return $value;
72 | }
73 |
74 | /**
75 | * Reverse Translate a value into its current locale.
76 | *
77 | * This feature can optionally be used when generating route urls by passing
78 | * the "translate" parameter to RouterInterface::generate()
79 | * specifying which attributes should be translated.
80 | *
81 | * @param string $route
82 | * @param string $locale
83 | * @param string $attribute
84 | * @param string $originalValue
85 | * @return string
86 | */
87 | public function reverseTranslate($route, $locale, $attribute, $value)
88 | {
89 | // values can potentially be large, so we hash them and prevent collisions
90 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $value;
91 | $cacheKey = "besimplei18nroute__reverse__" . sha1($hashKey);
92 | $reverseTranslatedValues = $this->cache->fetch($cacheKey);
93 | if ($reverseTranslatedValues && isset($reverseTranslatedValues[$hashKey])) {
94 | return $reverseTranslatedValues[$hashKey];
95 | }
96 |
97 | $query = "SELECT localized_value FROM routing_translations ".
98 | "WHERE route = ? AND locale = ? AND attribute = ? AND original_value = ?";
99 | if ($lovalizedValue = $this->connection->fetchColumn($query, array($route, $locale, $attribute, $value))) {
100 | $value = $lovalizedValue;
101 | }
102 |
103 | $reverseTranslatedValues[$hashKey] = $value;
104 | $this->cache->save($cacheKey, $reverseTranslatedValues);
105 |
106 | return $value;
107 | }
108 |
109 | public function addTranslation($route, $locale, $attribute, $localizedValue, $originalValue)
110 | {
111 | $query = "SELECT id FROM routing_translations WHERE route = ? AND locale = ? AND attribute = ?";
112 | $id = $this->connection->fetchColumn($query, array($route, $locale, $attribute));
113 |
114 | if ($id) {
115 | $this->connection->update('routing_translations', array(
116 | 'localized_value' => $localizedValue,
117 | 'original_value' => $originalValue,
118 | ), array('id' => $id));
119 | } else {
120 | $this->connection->insert('routing_translations', array(
121 | 'route' => $route,
122 | 'locale' => $locale,
123 | 'attribute' => $attribute,
124 | 'localized_value' => $localizedValue,
125 | 'original_value' => $originalValue,
126 | ));
127 | }
128 |
129 | // prime the cache!
130 | if ($this->primeCache) {
131 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $localizedValue;
132 | $cacheKey = "besimplei18nroute__" . sha1($hashKey);
133 | $translatedValues = $this->cache->fetch($cacheKey);
134 | if (!$translatedValues) {
135 | $translatedValues = array();
136 | }
137 | $translatedValues[$hashKey][$originalValue];
138 | $this->cache->save($cacheKey, $translatedValues);
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/Routing/RouteGenerator/I18nRouteGenerator.php:
--------------------------------------------------------------------------------
1 | routeNameInflector = $routeNameInflector ?: new PostfixInflector();
22 | }
23 |
24 | /**
25 | * @inheritdoc
26 | */
27 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute)
28 | {
29 | $collection = new RouteCollection();
30 |
31 | foreach ($localesWithPaths as $locale => $path) {
32 | /** @var \Symfony\Component\Routing\Route $localeRoute */
33 | $localeRoute = clone $baseRoute;
34 | $localeRoute->setDefault(self::LOCALE_PARAM, $locale);
35 | $localeRoute->setPath($path);
36 |
37 | $collection->add(
38 | $this->routeNameInflector->inflect($name, $locale),
39 | $localeRoute
40 | );
41 | }
42 |
43 | return $collection;
44 | }
45 |
46 | /**
47 | * Generate a localized version of the given route collection.
48 | *
49 | * @param array|string $prefix
50 | * @param RouteCollection $baseCollection
51 | * @return RouteCollection
52 | */
53 | public function generateCollection($prefix, RouteCollection $baseCollection)
54 | {
55 | $collection = clone $baseCollection;
56 |
57 | if (is_array($prefix)) {
58 | $prefixes = array();
59 | foreach ($prefix as $locale => $localePrefix) {
60 | $prefixes[$locale] = trim(trim($localePrefix), '/');
61 | }
62 |
63 | $this->localizeCollection($prefixes, $collection);
64 | } elseif (is_string($prefix) && preg_match(self::LOCALE_REGEX, $prefix)) {
65 | $originalPrefix = trim(trim($prefix), '/');
66 | $this->localizeCollectionLocaleParameter($originalPrefix, $collection);
67 | } else {
68 | // A normal prefix so just add it and return the original collection
69 | $collection->addPrefix($prefix);
70 | }
71 |
72 | return $collection;
73 | }
74 |
75 | /**
76 | * Localize a route collection.
77 | *
78 | * @param array $prefixes
79 | * @param RouteCollection $collection
80 | */
81 | protected function localizeCollection(array $prefixes, RouteCollection $collection)
82 | {
83 | $removeRoutes = array();
84 | $newRoutes = new RouteCollection();
85 | foreach ($collection->all() as $name => $route) {
86 | $routeLocale = $route->getDefault(self::LOCALE_PARAM);
87 | if ($routeLocale !== null) {
88 | if (!isset($prefixes[$routeLocale])) {
89 | throw new MissingRouteLocaleException(sprintf('Route `%s`: No prefix found for locale "%s".', $name, $routeLocale));
90 | }
91 |
92 | $route->setPath('/' . $prefixes[$routeLocale] . $route->getPath());
93 |
94 | continue;
95 | }
96 |
97 | // No locale found for the route so localize the route
98 | $removeRoutes[] = $name;
99 |
100 | foreach ($prefixes as $locale => $prefix) {
101 | /** @var \Symfony\Component\Routing\Route $localeRoute */
102 | $localeRoute = clone $route;
103 | $localeRoute->setPath('/' . $prefix . $route->getPath());
104 | $localeRoute->setDefault(self::LOCALE_PARAM, $locale);
105 |
106 | $newRoutes->add(
107 | $this->routeNameInflector->inflect($name, $locale),
108 | $localeRoute
109 | );
110 | }
111 | }
112 |
113 | $collection->remove($removeRoutes);
114 | $collection->addCollection($newRoutes);
115 | }
116 |
117 | /**
118 | * Localize the prefix `_locale` of all routes.
119 | *
120 | * @param string $prefix A prefix containing _locale
121 | * @param RouteCollection $collection A RouteCollection instance
122 | */
123 | protected function localizeCollectionLocaleParameter($prefix, RouteCollection $collection)
124 | {
125 | $localizedPrefixes = array();
126 | foreach ($collection->all() as $name => $route) {
127 | $locale = $route->getDefault(self::LOCALE_PARAM);
128 | if ($locale === null) {
129 | // No locale so nothing to do
130 | $routePrefix = $prefix;
131 | } else {
132 | // A locale was found so localize the prefix
133 | if (!isset($localizedPrefixes[$locale])) {
134 | $localizedPrefixes[$locale] = preg_replace(static::LOCALE_REGEX, $locale, $prefix);
135 | }
136 |
137 | $routePrefix = $localizedPrefixes[$locale];
138 | }
139 |
140 | $route->setPath('/' . $routePrefix . $route->getPath());
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class Configuration implements ConfigurationInterface
18 | {
19 | /**
20 | * Generates the configuration tree.
21 | *
22 | * @return TreeBuilder
23 | */
24 | public function getConfigTreeBuilder()
25 | {
26 | $treeBuilder = new TreeBuilder();
27 | $rootNode = $treeBuilder->root('be_simple_i18n_routing');
28 |
29 | $rootNode
30 | ->children()
31 | ->booleanNode('annotations')->defaultFalse()->end()
32 | ->scalarNode('route_name_inflector')->defaultValue('be_simple_i18n_routing.route_name_inflector.postfix')->end()
33 | ->end()
34 | ;
35 |
36 | $this->addLocalesSection($rootNode);
37 | $this->addAttributeTranslatorSection($rootNode);
38 |
39 | return $treeBuilder;
40 | }
41 |
42 | private function addLocalesSection(ArrayNodeDefinition $rootNode)
43 | {
44 | $rootNode
45 | ->children()
46 | ->arrayNode('locales')
47 | ->addDefaultsIfNotSet()
48 | ->children()
49 | ->scalarNode('default_locale')->defaultNull()->end()
50 | ->arrayNode('supported')
51 | ->treatNullLike(array())
52 | ->beforeNormalization()
53 | ->ifTrue(function ($v) { return !is_array($v); })
54 | ->then(function ($v) { return array($v); })
55 | ->end()
56 | ->prototype('scalar')->end()
57 | ->end()
58 |
59 | ->booleanNode('filter')
60 | ->defaultFalse()
61 | ->info(
62 | "set to true to filter out any unknown locales\n".
63 | "set to false to disable filtering locales"
64 | )
65 | ->end()
66 |
67 | ->scalarNode('strict')
68 | ->defaultFalse()
69 | ->validate()
70 | ->ifTrue(function ($v) { return $v !== null && !is_bool($v); })
71 | ->thenInvalid('Invalid type for path "strict". Expected boolean or null, but got %s.')
72 | ->end()
73 | ->info(
74 | "set to true to throw a exception when a i18n route is found where the locale is unknown or where a locale is missing\n".
75 | "set to false to disable exceptions so no locale missing or unknown exception are thrown\n".
76 | "set to null to disable locale is missing for a route exception\n".
77 | "'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production"
78 | )
79 | ->end()
80 | ->end()
81 | ->end()
82 | ->end();
83 | }
84 |
85 | private function addAttributeTranslatorSection(ArrayNodeDefinition $rootNode)
86 | {
87 | $rootNode
88 | ->children()
89 | ->arrayNode('attribute_translator')
90 | ->canBeEnabled()
91 | ->children()
92 | ->enumNode('type')
93 | ->defaultValue('translator')
94 | ->values(array('service', 'doctrine_dbal', 'translator'))
95 | ->end()
96 | ->scalarNode('id')->end()
97 | ->scalarNode('connection')->defaultNull()->end()
98 | ->arrayNode('cache')
99 | ->addDefaultsIfNotSet()
100 | ->beforeNormalization()
101 | ->ifString()
102 | ->then(function ($value) {
103 | return array('type' => $value);
104 | })
105 | ->end()
106 | ->children()
107 | ->enumNode('type')
108 | ->defaultValue('array')
109 | ->values(array('memcache', 'apc', 'array', 'xcache'))
110 | ->end()
111 | ->scalarNode('host')->end()
112 | ->scalarNode('port')->end()
113 | ->scalarNode('instance_class')->end()
114 | ->scalarNode('class')->end()
115 | ->end()
116 | ->end()
117 | ->end()
118 | ->validate()
119 | ->ifTrue(function ($value) {
120 | return 'service' === $value['type'] && !isset($value['id']);
121 | })
122 | ->thenInvalid('The id has to be specified to use a service as attribute translator')
123 | ->end()
124 | ->end()
125 | ->end();
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Routing/Router.php:
--------------------------------------------------------------------------------
1 | router = $router;
47 | $this->translator = $translator;
48 | $this->defaultLocale = $defaultLocale;
49 | $this->routeNameInflector = $routeNameInflector ?: new PostfixInflector();
50 | }
51 |
52 | /**
53 | * Generates a URL from the given parameters.
54 | *
55 | * @param string $name The name of the route
56 | * @param array $parameters An array of parameters
57 | * @param bool|int $referenceType The type of reference to be generated (one of the constants)
58 | *
59 | * @return string The generated URL
60 | *
61 | * @throws \InvalidArgumentException When the route doesn't exists
62 | */
63 | public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
64 | {
65 | if (isset($parameters['locale']) || isset($parameters['translate'])) {
66 | $locale = $this->getLocale($parameters);
67 |
68 | if (isset($parameters['locale'])) {
69 | unset($parameters['locale']);
70 | }
71 |
72 | if (null === $locale) {
73 | throw new MissingMandatoryParametersException('The locale must be available when using the "translate" option.');
74 | }
75 |
76 | if (isset($parameters['translate'])) {
77 | if (null !== $this->translator) {
78 | foreach ((array) $parameters['translate'] as $translateAttribute) {
79 | $parameters[$translateAttribute] = $this->translator->reverseTranslate(
80 | $name,
81 | $locale,
82 | $translateAttribute,
83 | $parameters[$translateAttribute]
84 | );
85 | }
86 | }
87 | unset($parameters['translate']);
88 | }
89 |
90 | return $this->generateI18n($name, $locale, $parameters, $referenceType);
91 | }
92 |
93 | try {
94 | return $this->router->generate($name, $parameters, $referenceType);
95 | } catch (RouteNotFoundException $e) {
96 | $locale = $this->getLocale($parameters);
97 | if (null !== $locale) {
98 | // at this point here we would never have $parameters['translate'] due to condition before
99 | return $this->generateI18n($name, $locale, $parameters, $referenceType);
100 | }
101 |
102 | throw $e;
103 | }
104 | }
105 |
106 | /**
107 | * {@inheritDoc}
108 | */
109 | public function match($pathinfo)
110 | {
111 | $match = $this->router->match($pathinfo);
112 |
113 | // if a _locale parameter isset remove the .locale suffix that is appended to each route in I18nRoute
114 | if (!empty($match['_locale']) && preg_match('#^(.+)\.'.preg_quote($match['_locale'], '#').'+$#', $match['_route'], $route)) {
115 | $match['_route'] = $route[1];
116 |
117 | // now also check if we want to translate parameters:
118 | if (null !== $this->translator && isset($match['_translate'])) {
119 | foreach ((array) $match['_translate'] as $attribute) {
120 | $match[$attribute] = $this->translator->translate(
121 | $match['_route'],
122 | $match['_locale'],
123 | $attribute,
124 | $match[$attribute]
125 | );
126 | }
127 | }
128 | }
129 |
130 | return $match;
131 | }
132 |
133 | public function getRouteCollection()
134 | {
135 | return $this->router->getRouteCollection();
136 | }
137 |
138 | public function setContext(RequestContext $context)
139 | {
140 | $this->router->setContext($context);
141 | }
142 |
143 | public function getContext()
144 | {
145 | return $this->router->getContext();
146 | }
147 |
148 | /**
149 | * Overwrite the locale to be used by default if the current locale could
150 | * not be found when building the route
151 | *
152 | * @param string $locale
153 | */
154 | public function setDefaultLocale($locale)
155 | {
156 | $this->defaultLocale = $locale;
157 | }
158 |
159 | /**
160 | * Generates a I18N URL from the given parameter
161 | *
162 | * @param string $name The name of the I18N route
163 | * @param string $locale The locale of the I18N route
164 | * @param array $parameters An array of parameters
165 | * @param bool|int $referenceType The type of reference to be generated (one of the constants)
166 | *
167 | * @return string The generated URL
168 | *
169 | * @throws RouteNotFoundException When the route doesn't exists
170 | */
171 | protected function generateI18n($name, $locale, $parameters, $referenceType = self::ABSOLUTE_PATH)
172 | {
173 | try {
174 | return $this->router->generate(
175 | $this->routeNameInflector->inflect($name, $locale),
176 | $parameters,
177 | $referenceType
178 | );
179 | } catch (RouteNotFoundException $e) {
180 | throw new RouteNotFoundException(sprintf('I18nRoute "%s" (%s) does not exist.', $name, $locale));
181 | }
182 | }
183 |
184 | /**
185 | * Determine the locale to be used with this request
186 | *
187 | * @param array $parameters the parameters determined by the route
188 | *
189 | * @return string
190 | */
191 | protected function getLocale($parameters)
192 | {
193 | if (isset($parameters['locale'])) {
194 | return $parameters['locale'];
195 | }
196 |
197 | if ($this->getContext()->hasParameter('_locale')) {
198 | return $this->getContext()->getParameter('_locale');
199 | }
200 |
201 | return $this->defaultLocale;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/Routing/Loader/AnnotatedRouteControllerLoader.php:
--------------------------------------------------------------------------------
1 | routeGenerator = $routeGenerator ?: new I18nRouteGenerator();
29 | $this->setRouteAnnotationClass('BeSimple\\I18nRoutingBundle\\Routing\\Annotation\\I18nRoute');
30 | }
31 |
32 | protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
33 | {
34 | /** @var \BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute $annot */
35 | $name = $annot->getName();
36 | if (null === $name) {
37 | $name = $this->getDefaultRouteName($class, $method);
38 | }
39 |
40 | $defaults = array_replace($globals['defaults'], $annot->getDefaults());
41 | foreach ($method->getParameters() as $param) {
42 | if (!isset($defaults[$param->getName()]) && $param->isDefaultValueAvailable()) {
43 | $defaults[$param->getName()] = $param->getDefaultValue();
44 | }
45 | }
46 | $requirements = array_replace($globals['requirements'], $annot->getRequirements());
47 | $options = array_replace($globals['options'], $annot->getOptions());
48 | $schemes = array_merge($globals['schemes'], $annot->getSchemes());
49 | $methods = array_merge($globals['methods'], $annot->getMethods());
50 |
51 | $host = $annot->getHost();
52 | if (null === $host) {
53 | $host = $globals['host'];
54 | }
55 |
56 | $condition = $annot->getCondition();
57 | if (null === $condition && isset($globals['condition'])) {
58 | $condition = $globals['condition'];
59 | }
60 |
61 | $path = '';
62 | $localesWithPaths = $annot->getLocales();
63 | if (is_scalar($localesWithPaths)) {
64 | $routePath = $localesWithPaths;
65 |
66 | if (!is_array($globals['locales'])) {
67 | // This is a normal route
68 | $path = $globals['locales'].$localesWithPaths;
69 | $localesWithPaths = null;
70 | } else {
71 | // Global contains the locales
72 | $localesWithPaths = array();
73 | foreach ($globals['locales'] as $locale => $localePath) {
74 | $localesWithPaths[$locale] = $localePath.$routePath;
75 | }
76 | }
77 | } elseif (is_array($localesWithPaths) && !empty($globals['locales'])) {
78 | if (!is_array($globals['locales'])) {
79 | // Global is a normal prefix
80 | foreach ($localesWithPaths as $locale => $localePath) {
81 | $localesWithPaths[$locale] = $globals['locales'].$localePath;
82 | }
83 | } else {
84 | foreach ($localesWithPaths as $locale => $localePath) {
85 | if (!isset($globals['locales'][$locale])) {
86 | throw new MissingLocaleException(sprintf('Locale "%s" for controller %s::%s is expected to be part of the global configuration at class level.', $locale, $class->getName(), $method->getName()));
87 | }
88 | $localesWithPaths[$locale] = $globals['locales'][$locale].$localePath;
89 | }
90 | }
91 | } elseif (!is_array($localesWithPaths)) {
92 | throw new MissingRouteLocaleException(sprintf('Missing locales for controller %s::%s', $class->getName(), $method->getName()));
93 | }
94 |
95 | $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
96 |
97 | $this->configureRoute($route, $class, $method, $annot);
98 |
99 | if (null === $localesWithPaths) {
100 | // Standard route
101 | $collection->add($name, $route);
102 |
103 | return;
104 | }
105 |
106 | $collection->addCollection(
107 | $this->routeGenerator->generateRoutes($name, $localesWithPaths, $route)
108 | );
109 | }
110 |
111 | /**
112 | * @inheritdoc
113 | */
114 | protected function getGlobals(\ReflectionClass $class)
115 | {
116 | $globals = array(
117 | 'locales' => '',
118 | 'requirements' => array(),
119 | 'options' => array(),
120 | 'defaults' => array(),
121 | 'schemes' => array(),
122 | 'methods' => array(),
123 | 'host' => '',
124 | 'condition' => '',
125 | );
126 |
127 | /** @var \BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute $annot */
128 | if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
129 | if (null !== $annot->getLocales()) {
130 | $globals['locales'] = $annot->getLocales();
131 | }
132 |
133 | if (null !== $annot->getRequirements()) {
134 | $globals['requirements'] = $annot->getRequirements();
135 | }
136 |
137 | if (null !== $annot->getOptions()) {
138 | $globals['options'] = $annot->getOptions();
139 | }
140 |
141 | if (null !== $annot->getDefaults()) {
142 | $globals['defaults'] = $annot->getDefaults();
143 | }
144 |
145 | if (null !== $annot->getSchemes()) {
146 | $globals['schemes'] = $annot->getSchemes();
147 | }
148 |
149 | if (null !== $annot->getMethods()) {
150 | $globals['methods'] = $annot->getMethods();
151 | }
152 |
153 | if (null !== $annot->getHost()) {
154 | $globals['host'] = $annot->getHost();
155 | }
156 |
157 | if (null !== $annot->getCondition()) {
158 | $globals['condition'] = $annot->getCondition();
159 | }
160 | }
161 |
162 | return $globals;
163 | }
164 |
165 | /**
166 | * Configures the _controller default parameter of a given Route instance.
167 | *
168 | * @param Route $route A route instance
169 | * @param \ReflectionClass $class A ReflectionClass instance
170 | * @param \ReflectionMethod $method A ReflectionClass method
171 | * @param mixed $annot The annotation class instance
172 | *
173 | * @throws \LogicException When the service option is specified on a method
174 | */
175 | protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot)
176 | {
177 | $route->setDefault('_controller', $class->getName().'::'.$method->getName());
178 | }
179 |
180 | /**
181 | * @inheritdoc
182 | *
183 | * @see \Sensio\Bundle\FrameworkExtraBundle\Routing\AnnotatedRouteControllerLoader::getDefaultRouteName
184 | */
185 | protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
186 | {
187 | $routeName = parent::getDefaultRouteName($class, $method);
188 |
189 | return preg_replace(array(
190 | '/(bundle|controller)_/',
191 | '/action(_\d+)?$/',
192 | '/__/',
193 | ), array(
194 | '_',
195 | '\\1',
196 | '_',
197 | ), $routeName);
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/DependencyInjection/BeSimpleI18nRoutingExtension.php:
--------------------------------------------------------------------------------
1 | load('routing.xml');
26 |
27 | $configuration = new Configuration();
28 | $config = $this->processConfiguration($configuration, $configs);
29 |
30 | $this->configureLocales($config, $container);
31 | $this->configureAttributeTranslator($config, $container, $loader);
32 | $this->configureRouteNameInflector($config, $container);
33 | $this->configureAnnotations($config, $container, $loader);
34 |
35 | if (PHP_VERSION_ID < 70000) {
36 | $this->addClassesToCompile(array(
37 | 'BeSimple\\I18nRoutingBundle\\Routing\\Router',
38 | 'BeSimple\\I18nRoutingBundle\\Routing\\RouteGenerator\\NameInflector\\RouteNameInflectorInterface'
39 | ));
40 | }
41 | }
42 |
43 | /**
44 | * Configures the attribute translator
45 | *
46 | * @param array $config
47 | * @param ContainerBuilder $container
48 | * @param LoaderInterface $loader
49 | */
50 | private function configureAttributeTranslator(array $config, ContainerBuilder $container, LoaderInterface $loader)
51 | {
52 | if (!isset($config['attribute_translator'])) {
53 | throw new \InvalidArgumentException('Expected attribute "attribute_translator" to be set');
54 | }
55 |
56 | $config = $config['attribute_translator'];
57 | if ($config['type'] === null) {
58 | return;
59 | }
60 |
61 | switch ($config['type']) {
62 | case 'service':
63 | $container->setAlias('be_simple_i18n_routing.translator', $config['id']);
64 | return;
65 |
66 | case 'doctrine_dbal':
67 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.connection_name', $config['connection']);
68 |
69 | $loader->load('dbal.xml');
70 |
71 | // BC support for symfony factory
72 | $def = $container->getDefinition('be_simple_i18n_routing.doctrine_dbal.connection');
73 |
74 | if (method_exists($def, 'setFactory')) {
75 | $def->setFactory(array(new Reference('doctrine'), 'getConnection'));
76 | } else {
77 | $def->setFactoryService('doctrine')
78 | ->setFactoryMethod('getConnection');
79 | }
80 |
81 | $this->configureDbalCacheDefinition($config['cache'], $container);
82 | $container->setAlias('be_simple_i18n_routing.translator', 'be_simple_i18n_routing.translator.doctrine_dbal');
83 |
84 | $attributes = array('event' => 'postGenerateSchema');
85 | if (null !== $config['connection']) {
86 | $attributes['connection'] = $config['connection'];
87 | }
88 | $def = $container->getDefinition('be_simple_i18n_routing.translator.doctrine_dbal.schema_listener');
89 | $def->addTag('doctrine.event_listener', $attributes);
90 | return;
91 |
92 | case 'translator':
93 | $container->setAlias('be_simple_i18n_routing.translator', 'be_simple_i18n_routing.translator.translation');
94 | return;
95 | }
96 |
97 | throw new \InvalidArgumentException(sprintf('Unsupported attribute translator type "%s"', $config['type']));
98 | }
99 |
100 | /**
101 | * Configures the Doctrine cache definition
102 | *
103 | * @param array $cacheDriver
104 | * @param ContainerBuilder $container
105 | */
106 | private function configureDbalCacheDefinition(array $cacheDriver, ContainerBuilder $container)
107 | {
108 | if ($cacheDriver['type'] === 'memcache') {
109 | if (!empty($cacheDriver['class'])) {
110 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache.class', $cacheDriver['class']);
111 | }
112 | if (!empty($cacheDriver['instance_class'])) {
113 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_instance.class', $cacheDriver['instance_class']);
114 | }
115 | if (!empty($cacheDriver['host'])) {
116 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_host', $cacheDriver['host']);
117 | }
118 | if (!empty($cacheDriver['port'])) {
119 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_port', $cacheDriver['port']);
120 | }
121 | }
122 |
123 | $container->setAlias('be_simple_i18n_routing.doctrine_dbal.cache', sprintf('be_simple_i18n_routing.doctrine_dbal.cache.%s', $cacheDriver['type']));
124 |
125 | // generate a unique namespace for the given application
126 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.namespace', 'be_simple_i18n_'.md5($container->getParameter('kernel.root_dir')));
127 | }
128 |
129 | /**
130 | * Configures the supported locales
131 | *
132 | * @param array $config
133 | * @param ContainerBuilder $container
134 | */
135 | private function configureLocales(array $config, ContainerBuilder $container)
136 | {
137 | if (!isset($config['locales'])) {
138 | throw new \InvalidArgumentException('Expected attribute "locales" to be set');
139 | }
140 | $config = $config['locales'];
141 |
142 | $container->setParameter('be_simple_i18n_routing.default_locale', $config['default_locale']);
143 | $container->setParameter('be_simple_i18n_routing.locales', $config['supported']);
144 |
145 | // Configure the route generator
146 | $routeGenerator = 'be_simple_i18n_routing.route_generator.i18n';
147 | if ($config['strict'] !== false) {
148 | $container->getDefinition('be_simple_i18n_routing.route_generator.strict')
149 | ->replaceArgument(0, new Reference($routeGenerator));
150 |
151 | if ($config['strict'] === null) {
152 | $container->getDefinition('be_simple_i18n_routing.route_generator.strict')
153 | ->addMethodCall('allowFallback', array(true));
154 | }
155 |
156 | $routeGenerator = 'be_simple_i18n_routing.route_generator.strict';
157 | }
158 | if ($config['filter']) {
159 | $container->getDefinition('be_simple_i18n_routing.route_generator.filter')
160 | ->replaceArgument(0, new Reference($routeGenerator));
161 |
162 | $routeGenerator = 'be_simple_i18n_routing.route_generator.filter';
163 | }
164 |
165 | $container->setAlias('be_simple_i18n_routing.route_generator', $routeGenerator);
166 | }
167 |
168 | /**
169 | * Configures the route name inflector
170 | *
171 | * @param ContainerBuilder $container
172 | * @param $config
173 | */
174 | private function configureRouteNameInflector(array $config, ContainerBuilder $container)
175 | {
176 | if (isset($config['route_name_inflector'])) {
177 | $container->setAlias('be_simple_i18n_routing.route_name_inflector', $config['route_name_inflector']);
178 | }
179 |
180 | if (PHP_VERSION_ID < 70000) {
181 | // Try and register the route name inflector to compilation/caching
182 | try {
183 | $def = $container->findDefinition('be_simple_i18n_routing.route_name_inflector');
184 | if ($def->getClass() !== null) {
185 | $this->addClassesToCompile(array($def->getClass()));
186 | }
187 | } catch (ServiceNotFoundException $e) {
188 | // This happens when the alias is set to a external service
189 | } catch (InvalidArgumentException $e) {
190 | // This happens when the alias is set to a external service in Symfony 2.3
191 | }
192 | }
193 | }
194 |
195 | /**
196 | * Configures the route annotations loader etc.
197 | *
198 | * @param array $config
199 | * @param ContainerBuilder $container
200 | * @param LoaderInterface $loader
201 | */
202 | private function configureAnnotations(array $config, ContainerBuilder $container, LoaderInterface $loader)
203 | {
204 | if (!isset($config['annotations']) || !$config['annotations']) {
205 | return;
206 | }
207 |
208 | $loader->load('annotation.xml');
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/Routing/Loader/YamlFileLoader.php:
--------------------------------------------------------------------------------
1 | routeGenerator = $routeGenerator ?: new I18nRouteGenerator();
36 | }
37 |
38 | /**
39 | * Loads a Yaml file.
40 | *
41 | * @param string $file A Yaml file path
42 | * @param string|null $type The resource type
43 | *
44 | * @return RouteCollection A RouteCollection instance
45 | *
46 | * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid
47 | */
48 | public function load($file, $type = null)
49 | {
50 | $path = $this->locator->locate($file);
51 |
52 | if (!stream_is_local($path)) {
53 | throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path));
54 | }
55 |
56 | if (!file_exists($path)) {
57 | throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path));
58 | }
59 |
60 | if (null === $this->yamlParser) {
61 | $this->yamlParser = new YamlParser();
62 | }
63 |
64 | try {
65 | $parsedConfig = $this->yamlParser->parse(file_get_contents($path));
66 | } catch (ParseException $e) {
67 | throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
68 | }
69 |
70 | $collection = new RouteCollection();
71 | $collection->addResource(new FileResource($path));
72 |
73 | // empty file
74 | if (null === $parsedConfig) {
75 | return $collection;
76 | }
77 |
78 | // not an array
79 | if (!is_array($parsedConfig)) {
80 | throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));
81 | }
82 |
83 | foreach ($parsedConfig as $name => $config) {
84 | if (isset($config['pattern'])) {
85 | if (isset($config['path'])) {
86 | throw new \InvalidArgumentException(sprintf('The file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path));
87 | }
88 |
89 | $config['path'] = $config['pattern'];
90 | unset($config['pattern']);
91 | }
92 |
93 | $this->validate($config, $name, $path);
94 |
95 | if (isset($config['resource'])) {
96 | $this->parseImport($collection, $config, $path, $file);
97 | } else {
98 | $this->parseRoute($collection, $name, $config, $path);
99 | }
100 | }
101 |
102 | return $collection;
103 | }
104 |
105 | /**
106 | * @inheritdoc
107 | */
108 | public function supports($resource, $type = null)
109 | {
110 | return 'be_simple_i18n' === $type && is_string($resource) && in_array(pathinfo($resource, PATHINFO_EXTENSION), array('yml', 'yaml'), true);
111 | }
112 |
113 | /**
114 | * Parses a route and adds it to the RouteCollection.
115 | *
116 | * @param RouteCollection $collection A RouteCollection instance
117 | * @param string $name Route name
118 | * @param array $config Route definition
119 | * @param string $path Full path of the YAML file being processed
120 | */
121 | protected function parseRoute(RouteCollection $collection, $name, array $config, $path)
122 | {
123 | $defaults = isset($config['defaults']) ? $config['defaults'] : array();
124 | $requirements = isset($config['requirements']) ? $config['requirements'] : array();
125 | $options = isset($config['options']) ? $config['options'] : array();
126 | $host = isset($config['host']) ? $config['host'] : '';
127 | $schemes = isset($config['schemes']) ? $config['schemes'] : array();
128 | $methods = isset($config['methods']) ? $config['methods'] : array();
129 | $condition = isset($config['condition']) ? $config['condition'] : null;
130 |
131 | if (isset($config['locales'])) {
132 | $collection->addCollection(
133 | $this->routeGenerator->generateRoutes(
134 | $name,
135 | $config['locales'],
136 | new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition)
137 | )
138 | );
139 | } else {
140 | $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
141 | $collection->add($name, $route);
142 | }
143 | }
144 |
145 | /**
146 | * Parses an import and adds the routes in the resource to the RouteCollection.
147 | *
148 | * @param RouteCollection $collection A RouteCollection instance
149 | * @param array $config Route definition
150 | * @param string $path Full path of the YAML file being processed
151 | * @param string $file Loaded file name
152 | */
153 | protected function parseImport(RouteCollection $collection, array $config, $path, $file)
154 | {
155 | $type = isset($config['type']) ? $config['type'] : null;
156 | $prefix = isset($config['prefix']) ? $config['prefix'] : '';
157 | $defaults = isset($config['defaults']) ? $config['defaults'] : array();
158 | $requirements = isset($config['requirements']) ? $config['requirements'] : array();
159 | $options = isset($config['options']) ? $config['options'] : array();
160 | $host = isset($config['host']) ? $config['host'] : null;
161 | $condition = isset($config['condition']) ? $config['condition'] : null;
162 | $schemes = isset($config['schemes']) ? $config['schemes'] : null;
163 | $methods = isset($config['methods']) ? $config['methods'] : null;
164 |
165 | $this->setCurrentDir(dirname($path));
166 |
167 | $subCollection = $this->import($config['resource'], $type, false, $file);
168 | /* @var $subCollection \Symfony\Component\Routing\RouteCollection */
169 | $subCollection = $this->routeGenerator->generateCollection($prefix, $subCollection);
170 | if (null !== $host) {
171 | $subCollection->setHost($host);
172 | }
173 | if (null !== $condition) {
174 | $subCollection->setCondition($condition);
175 | }
176 | if (null !== $schemes) {
177 | $subCollection->setSchemes($schemes);
178 | }
179 | if (null !== $methods) {
180 | $subCollection->setMethods($methods);
181 | }
182 | $subCollection->addDefaults($defaults);
183 | $subCollection->addRequirements($requirements);
184 | $subCollection->addOptions($options);
185 |
186 | $collection->addCollection($subCollection);
187 | }
188 |
189 | /**
190 | * @inheritDoc
191 | */
192 | protected function validate($config, $name, $path)
193 | {
194 | if (!is_array($config)) {
195 | throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
196 | }
197 | if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) {
198 | throw new \InvalidArgumentException(sprintf(
199 | 'The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".',
200 | $path,
201 | $name,
202 | implode('", "', $extraKeys),
203 | implode('", "', self::$availableKeys)
204 | ));
205 | }
206 | if (isset($config['resource']) && isset($config['path'])) {
207 | throw new \InvalidArgumentException(sprintf(
208 | 'The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.',
209 | $path,
210 | $name
211 | ));
212 | }
213 | if (!isset($config['resource']) && isset($config['type'])) {
214 | throw new \InvalidArgumentException(sprintf(
215 | 'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.',
216 | $name,
217 | $path
218 | ));
219 | }
220 | if (!isset($config['resource']) && !isset($config['path']) && !isset($config['locales'])) {
221 | throw new \InvalidArgumentException(sprintf(
222 | 'You must define a "path" for the route "%s" in file "%s".',
223 | $name,
224 | $path
225 | ));
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/Routing/Loader/XmlFileLoader.php:
--------------------------------------------------------------------------------
1 |
17 | * @author Francis Besset
18 | * @author Andrej Hudec
19 | */
20 | class XmlFileLoader extends FileLoader
21 | {
22 | const NAMESPACE_URI = 'http://besim.pl/schema/i18n_routing';
23 | const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
24 | /**
25 | * @var RouteGeneratorInterface
26 | */
27 | private $routeGenerator;
28 |
29 | public function __construct(FileLocatorInterface $locator, RouteGeneratorInterface $routeGenerator = null)
30 | {
31 | parent::__construct($locator);
32 |
33 | $this->routeGenerator = $routeGenerator ?: new I18nRouteGenerator();
34 | }
35 |
36 | /**
37 | * Loads an XML file.
38 | *
39 | * @param string $file An XML file path
40 | * @param string|null $type The resource type
41 | *
42 | * @return RouteCollection A RouteCollection instance
43 | *
44 | * @throws \InvalidArgumentException When the file cannot be loaded or when the XML cannot be
45 | * parsed because it does not validate against the scheme.
46 | */
47 | public function load($file, $type = null)
48 | {
49 | $path = $this->locator->locate($file);
50 |
51 | $xml = $this->loadFile($path);
52 |
53 | $collection = new RouteCollection();
54 | $collection->addResource(new FileResource($path));
55 |
56 | // process routes and imports
57 | foreach ($xml->documentElement->childNodes as $node) {
58 | if (!$node instanceof \DOMElement) {
59 | continue;
60 | }
61 |
62 | $this->parseNode($collection, $node, $path, $file);
63 | }
64 |
65 | return $collection;
66 | }
67 |
68 | /**
69 | * Parses a node from a loaded XML file.
70 | *
71 | * @param RouteCollection $collection Collection to associate with the node
72 | * @param \DOMElement $node Element to parse
73 | * @param string $path Full path of the XML file being processed
74 | * @param string $file Loaded file name
75 | *
76 | * @throws \InvalidArgumentException When the XML is invalid
77 | */
78 | protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
79 | {
80 | if (self::NAMESPACE_URI !== $node->namespaceURI) {
81 | return;
82 | }
83 |
84 | switch ($node->localName) {
85 | case 'route':
86 | $this->parseRoute($collection, $node, $path);
87 | break;
88 | case 'import':
89 | $this->parseImport($collection, $node, $path, $file);
90 | break;
91 | default:
92 | throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
93 | }
94 | }
95 |
96 | /**
97 | * @inheritdoc
98 | */
99 | public function supports($resource, $type = null)
100 | {
101 | return 'be_simple_i18n' === $type && is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
102 | }
103 |
104 | /**
105 | * Parses a route and adds it to the RouteCollection.
106 | *
107 | * @param RouteCollection $collection RouteCollection instance
108 | * @param \DOMElement $node Element to parse that represents a Route
109 | * @param string $path Full path of the XML file being processed
110 | *
111 | * @throws \InvalidArgumentException When the XML is invalid
112 | */
113 | protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path)
114 | {
115 | if ('' === ($id = $node->getAttribute('id'))) {
116 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path));
117 | }
118 |
119 | if ($node->hasAttribute('pattern')) {
120 | if ($node->hasAttribute('path')) {
121 | throw new \InvalidArgumentException(sprintf('The element in file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path));
122 | }
123 |
124 | $node->setAttribute('path', $node->getAttribute('pattern'));
125 | $node->removeAttribute('pattern');
126 | }
127 |
128 | $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY);
129 | $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY);
130 |
131 | list($defaults, $requirements, $options, $condition, $localesWithPaths) = $this->parseConfigs($node, $path);
132 |
133 | if ($localesWithPaths) {
134 | $collection->addCollection(
135 | $this->routeGenerator->generateRoutes(
136 | $id,
137 | $localesWithPaths,
138 | new Route('', $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition)
139 | )
140 | );
141 | } else {
142 | if (!$node->hasAttribute('pattern') && !$node->hasAttribute('path')) {
143 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "path" attribute.', $path));
144 | }
145 |
146 | $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
147 | $collection->add($id, $route);
148 | }
149 | }
150 |
151 | /**
152 | * Parses an import and adds the routes in the resource to the RouteCollection.
153 | *
154 | * @param RouteCollection $collection RouteCollection instance
155 | * @param \DOMElement $node Element to parse that represents a Route
156 | * @param string $path Full path of the XML file being processed
157 | * @param string $file Loaded file name
158 | *
159 | * @throws \InvalidArgumentException When the XML is invalid
160 | */
161 | protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file)
162 | {
163 | if ('' === $resource = $node->getAttribute('resource')) {
164 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute.', $path));
165 | }
166 |
167 | $type = $node->getAttribute('type');
168 | $prefix = $node->getAttribute('prefix');
169 | $host = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
170 | $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null;
171 | $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null;
172 |
173 | list($defaults, $requirements, $options, $condition, $localesWithPaths) = $this->parseConfigs($node, $path);
174 |
175 | $this->setCurrentDir(dirname($path));
176 |
177 | $subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file);
178 | /* @var $subCollection \Symfony\Component\Routing\RouteCollection */
179 | $subCollection = $this->routeGenerator->generateCollection(
180 | empty($localesWithPaths) ? $prefix : $localesWithPaths,
181 | $subCollection
182 | );
183 | if (null !== $host) {
184 | $subCollection->setHost($host);
185 | }
186 | if (null !== $condition) {
187 | $subCollection->setCondition($condition);
188 | }
189 | if (null !== $schemes) {
190 | $subCollection->setSchemes($schemes);
191 | }
192 | if (null !== $methods) {
193 | $subCollection->setMethods($methods);
194 | }
195 | $subCollection->addDefaults($defaults);
196 | $subCollection->addRequirements($requirements);
197 | $subCollection->addOptions($options);
198 |
199 | $collection->addCollection($subCollection);
200 | }
201 |
202 | /**
203 | * Loads an XML file.
204 | *
205 | * @param string $file An XML file path
206 | *
207 | * @return \DOMDocument
208 | *
209 | * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
210 | * or when the XML structure is not as expected by the scheme -
211 | * see validate()
212 | */
213 | protected function loadFile($file)
214 | {
215 | return XmlUtils::loadFile($file, __DIR__ . static::SCHEME_PATH);
216 | }
217 |
218 | /**
219 | * Parses the config elements (default, requirement, option).
220 | *
221 | * @param \DOMElement $node Element to parse that contains the configs
222 | * @param string $path Full path of the XML file being processed
223 | *
224 | * @return array An array with the defaults as first item, requirements as second and options as third.
225 | *
226 | * @throws \InvalidArgumentException When the XML is invalid
227 | */
228 | private function parseConfigs(\DOMElement $node, $path)
229 | {
230 | $defaults = array();
231 | $requirements = array();
232 | $options = array();
233 | $condition = null;
234 | $locales = array();
235 |
236 | foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
237 | switch ($n->localName) {
238 | case 'default':
239 | if ($this->isElementValueNull($n)) {
240 | $defaults[$n->getAttribute('key')] = null;
241 | } else {
242 | $defaults[$n->getAttribute('key')] = trim($n->textContent);
243 | }
244 |
245 | break;
246 | case 'requirement':
247 | $requirements[$n->getAttribute('key')] = trim($n->textContent);
248 | break;
249 | case 'option':
250 | $options[$n->getAttribute('key')] = trim($n->textContent);
251 | break;
252 | case 'condition':
253 | $condition = trim($n->textContent);
254 | break;
255 | case 'locale':
256 | $locales[$n->getAttribute('key')] = trim((string) $n->nodeValue);
257 | break;
258 | default:
259 | throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path));
260 | }
261 | }
262 |
263 | return array($defaults, $requirements, $options, $condition, $locales);
264 | }
265 |
266 | private function isElementValueNull(\DOMElement $element)
267 | {
268 | $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
269 |
270 | if (!$element->hasAttributeNS($namespaceUri, 'nil')) {
271 | return false;
272 | }
273 |
274 | return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | I18nRoutingBundle, generate your I18N Routes for Symfony2
2 | =========================================================
3 |
4 | If you have a website multilingual, this bundle avoids of copy paste your routes
5 | for different languages. Additionally it allows to translate given routing parameters
6 | between languages in Router#match and UrlGenerator#generate using either a Symfony Translator
7 | or a Doctrine DBAL (+Cache) based backend.
8 |
9 | [![Latest Version on Packagist][ico-version]][link-packagist]
10 | [![Software License][ico-license]](src/Resources/meta/LICENSE)
11 | [![Build Status][ico-travis]][link-travis]
12 | [![Total Downloads][ico-downloads]][link-packagist]
13 |
14 | ## Information
15 |
16 | When you create an I18N route and you go on it with your browser, the locale will be updated.
17 |
18 | ## Installation
19 |
20 | ```bash
21 | composer.phar require besimple/i18n-routing-bundle
22 | ```
23 |
24 | ```php
25 | //app/AppKernel.php
26 | public function registerBundles()
27 | {
28 | $bundles = array(
29 | //...
30 | new BeSimple\I18nRoutingBundle\BeSimpleI18nRoutingBundle(),
31 | );
32 | }
33 | ```
34 |
35 | ### Update your configuration
36 |
37 | ```yaml
38 | # app/config/config.yml
39 | be_simple_i18n_routing: ~
40 | ```
41 |
42 | ## Create your routing
43 |
44 | To define internationalized routes in XML or YAML, you need to import the
45 | routing file by using the ``be_simple_i18n`` type:
46 |
47 | ```yaml
48 | my_yaml_i18n_routes:
49 | resource: "@MyWebsiteBundle/Resources/config/routing/i18n.yml"
50 | type: be_simple_i18n
51 | prefix:
52 | en: /website
53 | fr: /site
54 | de: /webseite
55 | my_xml_i18n_routes:
56 | resource: "@MyWebsiteBundle/Resources/config/routing/i18n.xml"
57 | type: be_simple_i18n
58 | ```
59 |
60 | You can optionally specify a prefix or translated prefixes as shown above.
61 |
62 | ### Yaml routing file
63 |
64 | ```yaml
65 | homepage:
66 | locales: { en: "/welcome", fr: "/bienvenue", de: "/willkommen" }
67 | defaults: { _controller: MyWebsiteBundle:Frontend:index }
68 | ```
69 |
70 | ### XML routing file
71 |
72 | ```xml
73 |
74 |
77 |
78 |
79 | /welcome
80 | /bienvenue
81 | /willkommen
82 | MyWebsiteBundle:Frontend:index
83 |
84 |
85 | ```
86 |
87 | Note that the XML file uses a different namespace than when using the core
88 | loader: ``http://besim.pl/schema/i18n_routing``.
89 |
90 | ### PHP routing file
91 |
92 | ```php
93 | addCollection(
102 | $generator->generateRoutes(
103 | 'homepage',
104 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'),
105 | new Route('', array(
106 | '_controller' => 'MyWebsiteBundle:Frontend:index'
107 | ))
108 | )
109 | );
110 |
111 | return $collection;
112 | ```
113 |
114 | ### Controller annotations
115 |
116 | Annotation loading is only supported for Symfony 2.5 and greater and needs to be enabled as followed.
117 | ```YAML
118 | # app/config/config.yml
119 | be_simple_i18n_routing:
120 | annotations: true
121 | ```
122 |
123 | ```PHP
124 | use BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute;
125 |
126 | class NoPrefixController
127 | {
128 | /**
129 | * @I18nRoute({ "en": "/welcome", "fr": "/bienvenue", "de": "/willkommen" }, name="homepage")
130 | */
131 | public function indexAction() { }
132 | }
133 | ```
134 |
135 | ### You can insert classic route in your routing
136 |
137 | #### Yaml routing file
138 |
139 | ```yaml
140 | homepage:
141 | locales: { en: "/en/", fr: "/fr/", de: "/de/" }
142 | defaults: { _controller: HelloBundle:Frontend:homepage }
143 |
144 | welcome:
145 | locales: { en: "/welcome/{name}", fr: "/bienvenue/{name}", de: "/willkommen/{name}" }
146 | defaults: { _controller: MyWebsiteBundle:Frontend:welcome }
147 | ```
148 |
149 | #### XML routing file
150 |
151 | ```xml
152 |
153 |
154 |
157 |
158 |
159 | HelloBundle:Hello:index
160 |
161 |
162 | /welcome/{name}
163 | /bienvenue/{name}
164 | /willkommen/{name}
165 | MyWebsiteBundle:Frontend:index
166 |
167 |
168 | ```
169 |
170 | #### PHP routing file
171 |
172 | ```php
173 | add('hello', new Route('/hello/{name}', array(
183 | '_controller' => 'HelloBundle:Hello:index',
184 | )));
185 | $collection->addCollection(
186 | $generator->generateRoutes(
187 | 'homepage',
188 | array('en' => '/welcome/{name}', 'fr' => '/bienvenue/{name}', 'de' => '/willkommen/{name}'),
189 | new Route('', array(
190 | '_controller' => 'MyWebsiteBundle:Frontend:index',
191 | ))
192 | )
193 | );
194 |
195 | return $collection;
196 | ```
197 |
198 | ### Advanced locale support
199 |
200 | By default this bundle allows any locale to be used and there is no check if a locale is missing for a specific route.
201 | This is great but sometimes you may wish to be strict, let take a look at the following configuration:
202 | ```YAML
203 | be_simple_i18n_routing:
204 | locales:
205 | supported: ['en', 'nl']
206 | filter: true
207 | strict: true
208 | ```
209 |
210 | The `locales.supported` specifies which locales are supported.
211 |
212 | The `locales.filter` option is responsible for filtering out any unknown locales so only routes for 'en' and 'nl' are available.
213 |
214 | The `locales.strict` option when set to `true` is responsible for throwing a exception when a i18n route is found where the locale is unknown or where a locale is missing.
215 | This option can also be set to `null` to disable locale is missing for a route exception and `false` to disable exceptions.
216 |
217 | ### Route naming
218 |
219 | By default all routes that are imported are named '.' but sometimes you may want to change this behaviour.
220 | To do this you can specify a route name inflector service in your configuration as followed.
221 | ```YAML
222 | be_simple_i18n_routing:
223 | route_name_inflector: 'my_route_name_inflector_service'
224 | ```
225 | *The service must implement the `BeSimple\I18nRoutingBundle\Routing\RouteGenerator\NameInflector\RouteNameInflectorInterface` interface.*
226 |
227 | There are currently 2 inflectors available by default [`be_simple_i18n_routing.route_name_inflector.postfix`](src/Routing/RouteGenerator/NameInflector/PostfixInflector.php) and [`be_simple_i18n_routing.route_name_inflector.default_postfix`](src/Routing/RouteGenerator/NameInflector/DefaultPostfixInflector.php).
228 |
229 | #### Default postfix inflector
230 | The default postfix inflector changed the behaviour of to only add a locale postfix when the locale is not the default locale.
231 | A example configuration is as followed.
232 | ```YAML
233 | be_simple_i18n_routing:
234 | route_name_inflector: 'my_route_name_inflector_service'
235 | locales:
236 | default_locale: '%kernel.default_locale%'
237 | ```
238 |
239 | ## Generate a route in your templates
240 |
241 | ### Specify a locale
242 |
243 | #### Twig
244 |
245 | {{ path('homepage.en') }}
246 | {{ path('homepage', { 'locale': 'en' }) }}
247 | {{ path('homepage.fr') }}
248 | {{ path('homepage', { 'locale': 'fr' }) }}
249 | {{ path('homepage.de') }}
250 | {{ path('homepage', { 'locale': 'de' }) }}
251 |
252 | #### PHP
253 |
254 | ```php
255 | generate('homepage.en') ?>
256 | generate('homepage', array('locale' => 'en')) ?>
257 | generate('homepage.fr') ?>
258 | generate('homepage', array('locale' => 'fr')) ?>
259 | generate('homepage.de') ?>
260 | generate('homepage', array('locale' => 'de')) ?>
261 | ```
262 |
263 | ### Use current locale of user
264 |
265 | #### Twig
266 |
267 | {{ path('homepage') }}
268 |
269 | #### PHP
270 |
271 | ```php
272 | generate('homepage') ?>
273 | ```
274 |
275 | ## Translating the route attributes
276 |
277 | If the static parts of your routes are translated you get to the point really
278 | fast when dynamic parts such as product slugs, category names or other dynamic
279 | routing parameters should be translated. The bundle provides 2 implementations.
280 |
281 | After configuring the backend you want to use (see below for each one), you
282 | can define a to be translated attribute in your route defaults:
283 |
284 | ```yaml
285 | product_view:
286 | locales: { en: "/product/{slug}", de: "/produkt/{slug}" }
287 | defaults: { _controller: "ShopBundle:Product:view", _translate: "slug" }
288 | product_view2:
289 | locales: { en: "/product/{category}/{slug}", de: "/produkt/{category}/{slug}" }
290 | defaults:
291 | _controller: "ShopBundle:Product:view"
292 | _translate: ["slug", "category"]
293 | ```
294 |
295 | The same goes with generating routes, now backwards:
296 |
297 | {{ path("product_view", {"slug": product.slug, "translate": "slug"}) }}
298 | {{ path("product_view2", {"slug": product.slug, "translate": ["slug", "category]}) }}
299 |
300 | The reverse translation is only necessary if you have the "original" values
301 | in your templates. If you have access to the localized value of the current
302 | locale then you can just pass this and do not hint to translate it with the
303 | "translate" key.
304 |
305 | ### Doctrine DBAL Backend
306 |
307 | Configure the use of the DBAL backend
308 |
309 | ```yaml
310 | # app/config/config.yml
311 | be_simple_i18n_routing:
312 | attribute_translator:
313 | type: doctrine_dbal
314 | connection: default # Doctrine DBAL connection name. Using null (default value) will use the default connection
315 | cache: apc
316 | ```
317 |
318 | The Doctrine Backend has the following table structure:
319 |
320 | ```sql
321 | CREATE TABLE routing_translations (
322 | id INT NOT NULL,
323 | route VARCHAR(255) NOT NULL,
324 | locale VARCHAR(255) NOT NULL,
325 | attribute VARCHAR(255) NOT NULL,
326 | localized_value VARCHAR(255) NOT NULL,
327 | original_value VARCHAR(255) NOT NULL,
328 | UNIQUE INDEX UNIQ_291BA3522C420794180C698FA7AEFFB (route, locale, attribute),
329 | INDEX IDX_291BA352D951F3E4 (localized_value),
330 | PRIMARY KEY(id)
331 | ) ENGINE = InnoDB;
332 | ```
333 |
334 | Lookups are made through the combination of route name, locale and attribute
335 | of the route to be translated.
336 |
337 | Every lookup is cached in a Doctrine\Common\Cache\Cache instance that you
338 | should configure to be APC, Memcache or Xcache for performance reasons.
339 |
340 | If you are using Doctrine it automatically registers a listener for SchemaTool
341 | to create the routing_translations table for your database backend, you only
342 | have to call:
343 |
344 | ./app/console doctrine:schema:update --dump-sql
345 | ./app/console doctrine:schema:update --force
346 |
347 | ### Translator backend
348 |
349 | This implementation uses the Symfony2 translator to translate the attributes.
350 | The translation domain will be created using the pattern `_`
351 |
352 | ```yaml
353 | # app/config/config.yml
354 | be_simple_i18n_routing:
355 | attribute_translator:
356 | type: translator
357 | ```
358 |
359 | ### Custom backend
360 |
361 | If you want to use a different implementation, simply create a service implementing
362 | `BeSimple\I18nRoutingBundle\Routing\Translator\AttributeTranslatorInterface`.
363 |
364 | ```yaml
365 | # app/config/config.yml
366 | be_simple_i18n_routing:
367 | attribute_translator:
368 | type: service
369 | id: my_attribute_translator
370 | ```
371 |
372 |
373 | ## License
374 |
375 | This bundle is under the MIT License (MIT). Please see [License File](src/Resources/meta/LICENSE) for more information.
376 |
377 | [ico-version]: https://img.shields.io/packagist/v/BeSimple/i18n-routing-bundle.svg?style=flat-square
378 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
379 | [ico-travis]: https://img.shields.io/travis/BeSimple/BeSimpleI18nRoutingBundle/master.svg?style=flat-square
380 | [ico-downloads]: https://img.shields.io/packagist/dt/BeSimple/i18n-routing-bundle.svg?style=flat-square
381 |
382 | [link-packagist]: https://packagist.org/packages/BeSimple/i18n-routing-bundle
383 | [link-travis]: https://travis-ci.org/BeSimple/BeSimpleI18nRoutingBundle
384 |
--------------------------------------------------------------------------------