Last active
March 17, 2020 21:02
-
-
Save Itach1Uchixa/6ec75b8f2af47a0b63e3f52fa0285a24 to your computer and use it in GitHub Desktop.
Zend Framework 3 Localized route concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Kakhramonov Javlonbek <[email protected]> | |
* @version 1.1.2 | |
*/ | |
namespace Application\Factory; | |
use Interop\Container\ContainerInterface; | |
use Application\Router\LocalizedTreeRouteStack; | |
use Zend\Router\RouterConfigTrait; | |
use Zend\Router\RouteStackInterface; | |
use Zend\ServiceManager\Factory\FactoryInterface; | |
class LocalizedTreeRouteFactory implements FactoryInterface | |
{ | |
use RouterConfigTrait; | |
/** | |
* Create and return the HTTP router | |
* | |
* Retrieves the "router" key of the Config service, and uses it | |
* to instantiate the router. Uses the TreeRouteStack implementation by | |
* default. | |
* | |
* @param ContainerInterface $container | |
* @param string $name | |
* @param null|array $options | |
* @return RouteStackInterface | |
*/ | |
public function __invoke(ContainerInterface $container, $name, array $options = null) | |
{ | |
$config = $container->has('config') ? $container->get('config') : []; | |
$translatorConfig = $config['translator']; | |
$class = LocalizedTreeRouteStack::class; | |
$config = isset($config['router']) ? $config['router'] : []; | |
// Default locale of the route | |
$config['default_locale'] = $translatorConfig['locale']; | |
/** | |
* You can change it to fit to your logic | |
* in this case application config has | |
* $config['translator']['locales'] key that has structure: | |
* | |
* [ | |
* 'locale_name' => 'Language name', | |
* 'locale_name2' => 'Language name 2', | |
* ] | |
* | |
* Notice that 'locale_name' key will be used by router to assemble | |
* and match routes e.g route will be like: | |
* locale_name/your/routes or | |
* /your/routes for default locale | |
*/ | |
$config['supported_locales'] = array_flip($translatorConfig['locales']); | |
return $this->createRouter($class, $config, $container); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Kakhramonov Javlonbek <[email protected]> | |
* @version 1.1.0 | |
*/ | |
namespace Application\Router; | |
use Zend\Router\Http\RouteMatch; | |
use Zend\Router\Http\TreeRouteStack; | |
use Zend\Stdlib\RequestInterface as Request; | |
use Zend\Uri\Uri; | |
use Zend\Uri\UriFactory; | |
use Application\Exception; | |
class LocalizedTreeRouteStack extends TreeRouteStack | |
{ | |
/** | |
* @var string | |
*/ | |
protected $locale; | |
/** | |
* @var string[] | |
*/ | |
protected $supportedLocales; | |
/** | |
* @var string | |
*/ | |
protected $defaultLocale; | |
/** | |
* @inheritDoc | |
*/ | |
public static function factory($options = []) | |
{ | |
/** | |
* @var LocalizedTreeRouteStack $instance | |
*/ | |
$instance = parent::factory($options); | |
if (!isset($options['default_locale']) || !is_string($options['default_locale'])) { | |
throw new Exception\RuntimeException( | |
sprintf( | |
"%s::factory expects 'default_locale' option to be string", | |
self::class | |
) | |
); | |
} else { | |
$instance->setDefaultLocale($options['default_locale']); | |
} | |
if (!isset($options['supported_locales']) | |
|| !is_array($options['supported_locales']) | |
|| empty($options['supported_locales']) | |
) { | |
throw new Exception\RuntimeException( | |
sprintf( | |
"%s::factory expects 'supported_locales' option to be array", | |
self::class | |
) | |
); | |
} else { | |
$instance->setSupportedLocales($options['supported_locales']); | |
} | |
return $instance; | |
} | |
/** | |
* Returns requested route locale | |
* | |
* @return string | |
*/ | |
public function getLocale() | |
{ | |
return $this->locale; | |
} | |
/** | |
* @return string[] | |
*/ | |
public function getSupportedLocales() | |
{ | |
return $this->supportedLocales; | |
} | |
/** | |
* @param string[] $supportedLocales | |
* | |
* @return LocalizedTreeRouteStack | |
*/ | |
public function setSupportedLocales($supportedLocales) | |
{ | |
$this->supportedLocales = $supportedLocales; | |
return $this; | |
} | |
/** | |
* @return string | |
*/ | |
public function getDefaultLocale() | |
{ | |
return $this->defaultLocale; | |
} | |
/** | |
* @param string $defaultLocale | |
* | |
* @return LocalizedTreeRouteStack | |
*/ | |
public function setDefaultLocale($defaultLocale) | |
{ | |
$this->defaultLocale = $defaultLocale; | |
return $this; | |
} | |
/** | |
* Simple modification for localization | |
* Adds locale to the uri | |
* | |
* @inheritdoc | |
*/ | |
public function assemble(array $params = [], array $options = []) | |
{ | |
$assembled = parent::assemble($params, $options); | |
$locale = $this->locale; | |
$supported = $this->getSupportedLocales(); | |
if (isset($options['locale']) && $options['locale']) { | |
$locale = trim($options['locale']); | |
} | |
$isDefaultNeeded = isset($options['force_canonical']) && $options['force_canonical']; | |
$isDefaultNeeded = $isDefaultNeeded || isset($options['keep_default_locale']); | |
$isDefaultNeeded = $isDefaultNeeded && $options['keep_default_locale']; | |
// if it is not required remove default locale | |
if (!$isDefaultNeeded && $locale === $this->getDefaultLocale()) { | |
unset($locale); | |
} | |
if (isset($locale) && $locale) { | |
if (!in_array($locale, $supported)) { | |
throw new Exception\RuntimeException( | |
sprintf("Invalid locale '%s'", $locale) | |
); | |
} | |
/** | |
* @var Uri $uri | |
*/ | |
$uri = UriFactory::factory($assembled); | |
$parts = array($this->getBaseUrl(), $locale); | |
$parts[] = ltrim(ltrim($uri->getPath(), $this->getBaseUrl()), '/'); | |
return $uri->setPath(join('/', $parts))->toString(); | |
} | |
return $assembled; | |
} | |
/** | |
* @inheritdoc | |
*/ | |
public function match(Request $request, $pathOffset = null, array $options = []) | |
{ | |
$locale = $this->localeFromRequest($request); | |
$this->locale = $locale ?: $this->getDefaultLocale(); | |
$pathOffset = $locale ? ($pathOffset ?: 0) + (strlen($locale) + 1) : null; | |
$routeMatch = parent::match($request, $pathOffset, $options); | |
// add locale param to route match | |
if ($routeMatch instanceof RouteMatch && $this->locale) { | |
$routeMatch->setParam('locale', $this->locale); | |
} | |
return $routeMatch; | |
} | |
/** | |
* Returns matched locale from | |
* | |
* @param Request $request | |
* | |
* @return string|null | |
*/ | |
private function localeFromRequest(Request $request) | |
{ | |
/** | |
* @var \Zend\Http\PhpEnvironment\Request $request | |
*/ | |
$scriptName = $request->getServer('SCRIPT_NAME'); | |
// trim base path | |
$path = ltrim($request->getUri()->getPath(), substr($scriptName, 0, strrpos($scriptName, '/'))); | |
$path = ltrim($path, '/'); | |
$locale = substr($path, 0, strpos($path, '/')); | |
$locales = $this->getSupportedLocales(); | |
if ($locale && in_array($locale, $locales)) { | |
return $locale; | |
} | |
return null; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Kakhramonov Javlonbek <[email protected]> | |
* @version 1.1.0 | |
*/ | |
namespace Application\Listener; | |
use Interop\Container\ContainerInterface; | |
use Zend\EventManager\AbstractListenerAggregate; | |
use Zend\EventManager\EventManagerInterface; | |
use Zend\I18n\Translator\Translator; | |
use Zend\Mvc\MvcEvent; | |
/** | |
* Example route listener to prepare locale settings | |
* Change it the way you want don't forget to attach it to your EventManager | |
*/ | |
class RouteListener extends AbstractListenerAggregate | |
{ | |
/** | |
* @param EventManagerInterface $events | |
* @param int $priority | |
*/ | |
public function attach(EventManagerInterface $events, $priority = 1) | |
{ | |
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'afterRoute'], -1001); | |
} | |
public function afterRoute(MvcEvent $e) | |
{ | |
$args = $e->getParams(); | |
$router = $args['router']; | |
$this->setLocale($router->getLocale(), $e->getApplication()->getServiceManager()); | |
} | |
/** | |
* Sets locale where needed | |
* | |
* @param string $locale | |
* @param ContainerInterface $services | |
* | |
* @return void | |
*/ | |
private function setLocale($locale, ContainerInterface $services) | |
{ | |
$translator = $services->get(Translator::class); | |
$translator->setLocale($locale); | |
if ($services->has(\Gedmo\Translatable\TranslatableListener::class)) { | |
// In case you have Doctrine extensions translatable listener | |
// by default Doctrine extensions don't have such service | |
// I myself registered such service | |
// remove these lines if you don't have it | |
$services | |
->get(\Gedmo\Translatable\TranslatableListener::class) | |
->setTranslatableLocale($locale); | |
} | |
} | |
} |
To make it work
1 step. Add line below to your service_manager config
Zend\Router\Http\TreeRouteStack::class => LocalizedTreeRouteFactory::class
2 step. Register listener to your event manager
RouteListener
3 step. Your translator config must have locales key that must be like
'translator' => [
// this will be default locale of route
'locale' => 'en_US',
// key must be locale that you want
'locales' => [
'en_US' => 'English',
'fr' => 'French',
'ru_RU' => 'Russian'
],
],
Step 3 is optional you can change route factory to use another config
The config above will make you route:
/your/route or en_US/your/route for English
/fr/your/route for French
/ru_RU/your/route for Russian
You can ask me anything if you would have any question.
Hi! Under which license do you publish the code? Thank you.
Feel free to use it but under one condition use it for good purposes only
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I need such thing nearly in every of my projects. So that I decided to publish my concept. I will be happy if you help me improve it.