Убираем default locale из url в Symfony

Недавно столкнулся с интересной задачей и, думаю, что решение может оказаться кому-то полезным.

Наш сайт использует локализацию + региональность и соответственно ссылки имеют вид:
/ru/minsk/aaa
/by/minsk/aaa
Задача убрать дефолтную локаль(ru) так чтобы все url приняли вид:
/minsk/aaa
/by/minsk/aaa
Stackoverflow дает два варианта:
1) В описании роута в requirements в _locale добавить пустое значение: _locale: ru|by -> _locale: |ru|by
Но в этом подходе url принимает вид: //minsk/aaa, а 2 слеша нам не нужны. Да и нужно убирать локаль из router->context, чтобы сгенерировать url.
2) В аннотациях к контроллеру прописать 2 разных роута.

* @Route("/{region}/{other}", name="route", ...)
* @Route("/{_locale}/{region}/{other}", name="route-with-locale", ...)

Но здесь тоже встает вопрос: «Как генерировать url и какой роут выбирать?». Да и для всех контроллеров придется такие аннотации писать.

Решение состоит из 2х этапов: обработка входящих параметров путем подмены контроллера и генерация ссылок на странице.

Настраивать будем все ресурсы относящиеся к префиксу "/{_locale}/{region}" и которые хранятся в одной папке и описаны в app_web_common:

app_web_common:
    resource: "@AppBundle/Controller/Web/"
    type:     annotation
    prefix:   /{_locale}/{region}
    requirements:
        _locale: ru|by
        region: minsk|brest

1. Обработка входящих параметров
Все url вида "/by/minsk/aaa" будут обрабатываться по обычной схеме через «app_web_common». А урлы, не содержащие локаль, через отдельный роут:

app_web_region_no_locale:
    path:   /{region}/{catchAll}
    defaults:
        _locale: ru|by
        _controller: AppBundle:NoLocale:region
    requirements:
        region: minsk|brest
        catchAll: ".+"

Но вызов контроллера производиться не будет(кроме 404), тк мы перенаправим вызов к другому контроллеру в KernelEvents::CONTROLLER. Поэтому создаем EventListener со своим обработчиком:

    public function matchingWebController(FilterControllerEvent $event)
    {
        $request = $event->getRequest();
        $route = $request->get('_route');
        $pathInfo = $request->getPathInfo();

        if ($route == 'app_web_region_no_locale') {
            $this->setupLocale($this->defaultLocale, $request);
            $path = "/{$this->defaultLocale}{$pathInfo}";

           $data = $this->router->match($path);

           $request->attributes->replace($data);
           unset($data['_controller']);
           $request->attributes->set('_route_params', $data);

           $controller = $this->resolver->getController($request);
           $event->setController($controller);
        }
    }

Если кратко, то:
— берем текущий путь
— добавляем в начало строки префикс локали
— ищем совпадения для нового урла
— заменяем данные в реквесте
— заменяем контроллер

Еще нужно запретить переход по ссылке содержащей дефолтную локаль. Для этого в этом же классе добавим еще один обработчик, но уже KernelEvents::REQUEST:

    public function redirectFromDefaultLocale(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $queryString = $request->getQueryString();
        $pathInfo = $request->getPathInfo();
        $pathInfoArr = explode('/', $pathInfo);
        $queryString = $queryString ? "?" . $queryString : '';

        // if route contains default locale
        if (isset($pathInfoArr[1]) && $pathInfoArr[1] == $this->defaultLocale) {
            unset($pathInfoArr[1]);
            $this->setupLocale($this->defaultLocale, $request);
            $response = new RedirectResponse($pathInfo . $queryString);
            $event->setResponse($response);
        }
    }

Но все это действительно если есть префикс ввиде {region} как в статье. Но как быть если нет этого префикса?
/ru/aaa
/by/aaa
а нужно:
/aaa
/by/aaa
Здесь все зависит от приложения:
1) можно точно также все перенаправить через app_web_*_no_locale, но уже без региона.
2) Конкретно в нашем приложении есть еще и урлы без локали и региона (/aaa), поэтому мы сначала обрабатываем их, а затем если не находим, то применяем логику из «matchingWebController()»

2. Генерация ссылок
Для этого нужно переопределить класс UrlGenerator.
В параметры добавляем:

    router.class: AppBundle\Routing\Router\Router
    router.options.generator_base_class: AppBundle\Routing\Generator\UrlGenerator\UrlGenerator

И содержимое классов:

class Router extends BaseRouter implements ContainerAwareInterface
{
    private $container;

    public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null)
    {
        parent::__construct($container, $resource, $options, $context);
        $this->setContainer($container);
    }

    public function getGenerator()
    {
        $generator = parent::getGenerator();

        $generator->setContainer($this->container);
        return $generator;
    }

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
}

class UrlGenerator extends BaseUrlGenerator implements ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    private $container;

    protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
    {
        $defaultLocale = $this->container->getParameter('default_locale');
        // здесь все содержимое родительского метода parent::doGenerate
        // ....
        // заменяем одну строчку $url = $token[1].$mergedParams[$token[3]].$url; на 
        if (!($token[3] == '_locale' && $mergedParams[$token[3]] == $defaultLocale)) {
            $url = $token[1].$mergedParams[$token[3]].$url;
        }
        // ....
    }

    /**
     * @param ContainerInterface|null $container
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
}


Если кратко, то если в полученном урле есть дефолтная локаль, то ее удаляем.

Вот и всё.
Tags:
symfony

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.