Skip to content

Instantly share code, notes, and snippets.

@Itach1Uchixa
Last active March 17, 2020 21:02
Show Gist options
  • Save Itach1Uchixa/6ec75b8f2af47a0b63e3f52fa0285a24 to your computer and use it in GitHub Desktop.
Save Itach1Uchixa/6ec75b8f2af47a0b63e3f52fa0285a24 to your computer and use it in GitHub Desktop.
Zend Framework 3 Localized route concept
<?php
/**
* @author Kakhramonov Javlonbek <kakjavlon@gmail.com>
* @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);
}
}
<?php
/**
* @author Kakhramonov Javlonbek <kakjavlon@gmail.com>
* @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;
}
}
<?php
/**
* @author Kakhramonov Javlonbek <kakjavlon@gmail.com>
* @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);
}
}
}
@Itach1Uchixa
Copy link
Author

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.

@Itach1Uchixa
Copy link
Author

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.

@abePdIta
Copy link

abePdIta commented Jan 19, 2018

Hi! Under which license do you publish the code? Thank you.

@Itach1Uchixa
Copy link
Author

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