Symfony 3 перенаправляет все маршруты на текущую версию локали

Я работаю над приложением symfony, где моя цель состоит в том, что независимо от того, на какой странице находится пользователь, он перейдет к языковой версии страницы.

Например, если пользователь переходит на «/» на домашней странице, он будет перенаправлен на «/en/».

Если они находятся на странице "/admin", они будут перенаправлены на "/en/admin" таким образом, что свойство _locale будет установлено из маршрута.

Также ему необходимо определить локаль, если они посещают /admin из браузера пользователей, поскольку локаль не была определена, поэтому он знает, на какую страницу перенаправить.

В настоящее время мой контроллер по умолчанию выглядит так, как показано ниже, так как я тестирую. Я использую режим разработки и профилировщик, чтобы проверить, правильно ли работают переводы.

введите здесь описание изображения

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     * @Route("/{_locale}/", name="homepage_locale")
     */
    public function indexAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }
}

Этот текущий метод будет удерживать пользователя в «/», если он перейдет туда, но я хочу, чтобы он перенаправлялся на «/en/». Это должно работать и для других страниц, таких как /admin или /somepath/pathagain/article1 (/en/admin , /en/somepath/pathagain/article1)

Как мне это сделать?

Ссылки, которые я читал, не помогли:

Symfony2 Использовать локаль по умолчанию в маршрутизации (один URL-адрес для один язык)

Локаль Symfony2 по умолчанию в маршрутизации

::Обновление::

Я не решил свою проблему, но я приблизился к ней, а также научился нескольким приемам, чтобы быть более эффективным.

DefaultController.php

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{

    /**
     * @Route("/", name="home", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
     * @Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
     */
    public function indexAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }

    /**
     * @Route("/admin", name="admin", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
     * @Route("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
     */
    public function adminAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }
}
?>

Config.yml

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
    locale: en
    app.locales: en|es|zh

framework:
    #esi:             ~
    translator:      { fallbacks: ["%locale%"] }
    secret:          "%secret%"
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: ~
    form:            ~
    csrf_protection: ~
    validation:      { enable_annotations: true }
    #serializer:      { enable_annotations: true }
    templating:
        engines: ['twig']
        #assets_version: SomeVersionScheme
    default_locale:  "%locale%"
    trusted_hosts:   ~
    trusted_proxies: ~
    session:
        # handler_id set to null will use default session handler from php.ini
        handler_id:  ~
        save_path:   "%kernel.root_dir%/../var/sessions/%kernel.environment%"
    fragments:       ~
    http_method_override: true
    assets: ~

# Twig Configuration
twig:
    debug:            "%kernel.debug%"
    strict_variables: "%kernel.debug%"

# Doctrine Configuration
doctrine:
    dbal:
        driver:   pdo_mysql
        host:     "%database_host%"
        port:     "%database_port%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"
        charset:  UTF8
        # if using pdo_sqlite as your database driver:
        #   1. add the path in parameters.yml
        #     e.g. database_path: "%kernel.root_dir%/data/data.db3"
        #   2. Uncomment database_path in parameters.yml.dist
        #   3. Uncomment next line:
        #     path:     "%database_path%"

    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true

# Swiftmailer Configuration
swiftmailer:
    transport: "%mailer_transport%"
    host:      "%mailer_host%"
    username:  "%mailer_user%"
    password:  "%mailer_password%"
    spool:     { type: memory }

Обратите внимание на значение app.locales: en|es|zh под параметрами. Теперь это значение, на которое я могу ссылаться всякий раз, когда я создаю свои маршруты, если я планирую поддерживать больше локалей в будущем, что я и делаю. Эти маршруты английский, испанский, китайский в порядке для тех, кто любопытен. В DefaultController в аннотациях "%app.locales%" — это часть, которая ссылается на параметр конфигурации.

Проблема с моим текущим методом заключается в том, что /admin, например, не перенаправляет пользователя на /{locale браузеров}/admin, что было бы более элегантным решением для организации всего... но, по крайней мере, маршруты работают. Все еще ищет лучшее решение.

****Обновлять****

Я думаю, что, возможно, нашел ответ здесь как нижний ответ (Добавьте локаль и требования ко всем маршрутам — Symfony2), ответ Athlan. Просто не уверен, как реализовать это в symfony 3, так как его указания были мне недостаточно ясны.

Я думаю, что эта статья также может помочь (http://symfony.com/doc/current/components/event_dispatcher/introduction.html)


person Joseph Astrahan    schedule 10.01.2016    source источник


Ответы (4)


У меня недостаточно репутации, чтобы добавить комментарий к правильному решению. Поэтому я добавляю новый ответ

Вы можете добавить «префикс: /{_locale}» в app/config/routing.yml следующим образом:

app:
    resource: "@AppBundle/Controller/"
    type:     annotation
    prefix:   /{_locale}

Поэтому вам не нужно добавлять его в каждый маршрут для каждого действия. Для следующих шагов. Спасибо большое все идеально.

person Susana Santos    schedule 16.08.2016
comment
Теперь у тебя достаточно репутации :). - person Joseph Astrahan; 11.02.2018

После 12 часов изучения этого я наконец нашел приемлемое решение. Пожалуйста, опубликуйте пересмотренные версии этого решения, если вы можете сделать его более эффективным.

Некоторые вещи, чтобы отметить, мое решение зависит от моей потребности. Что он делает, так это заставляет любой URL перейти к локализованной версии, если она существует.

Это требует соблюдения некоторых соглашений при создании маршрутов.

DefaultController.php

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{

    /**
     * @Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
     */
    public function indexAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }

    /**
     * @Route("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
     */
    public function adminAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }
}
?>

Обратите внимание, что оба маршрута всегда начинаются с «/{_locale}/». Чтобы это работало, каждый маршрут в вашем проекте должен иметь это. Вы просто указываете настоящее имя маршрута после этого. Для меня я был в порядке с этим сценарием. Вы можете легко изменить мое решение, чтобы оно соответствовало вашим потребностям.

Первым шагом является создание прослушивания на httpKernal для перехвата запросов до того, как они перейдут на маршрутизаторы для их обработки.

LocaleRewriteListener.php

<?php
//src/AppBundle/EventListener/LocaleRewriteListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;

use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;

class LocaleRewriteListener implements EventSubscriberInterface
{
    /**
     * @var Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
    * @var routeCollection \Symfony\Component\Routing\RouteCollection
    */
    private $routeCollection;

    /**
     * @var string
     */
    private $defaultLocale;

    /**
     * @var array
     */
    private $supportedLocales;

    /**
     * @var string
     */
    private $localeRouteParam;

    public function __construct(RouterInterface $router, $defaultLocale = 'en', array $supportedLocales = array('en'), $localeRouteParam = '_locale')
    {
        $this->router = $router;
        $this->routeCollection = $router->getRouteCollection();
        $this->defaultLocale = $defaultLocale;
        $this->supportedLocales = $supportedLocales;
        $this->localeRouteParam = $localeRouteParam;
    }

    public function isLocaleSupported($locale) 
    {
        return in_array($locale, $this->supportedLocales);
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        //GOAL:
        // Redirect all incoming requests to their /locale/route equivlent as long as the route will exists when we do so.
        // Do nothing if it already has /locale/ in the route to prevent redirect loops

        $request = $event->getRequest();
        $path = $request->getPathInfo();

        $route_exists = false; //by default assume route does not exist.

        foreach($this->routeCollection as $routeObject){
            $routePath = $routeObject->getPath();
            if($routePath == "/{_locale}".$path){
                $route_exists = true;
                break;
            }
        }

        //If the route does indeed exist then lets redirect there.
        if($route_exists == true){
            //Get the locale from the users browser.
            $locale = $request->getPreferredLanguage();

            //If no locale from browser or locale not in list of known locales supported then set to defaultLocale set in config.yml
            if($locale==""  || $this->isLocaleSupported($locale)==false){
                $locale = $request->getDefaultLocale();
            }

            $event->setResponse(new RedirectResponse("/".$locale.$path));
        }

        //Otherwise do nothing and continue on~
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered before the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
        );
    }
}

Наконец, вы устанавливаете services.yml для запуска слушателя.

Services.yml

# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
#    parameter_name: value

services:
#    service_name:
#        class: AppBundle\Directory\ClassName
#        arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
     appBundle.eventListeners.localeRewriteListener:
          class: AppBundle\EventListener\LocaleRewriteListener
          arguments: ["@router", "%kernel.default_locale%", "%locale_supported%"]
          tags:
            - { name: kernel.event_subscriber }

Также в config.yml вы захотите добавить следующие параметры:

config.yml

parameters:
    locale: en
    app.locales: en|es|zh
    locale_supported: ['en','es','zh']

Я хотел, чтобы было только одно место, где вы определяете локали, но мне пришлось сделать 2... но, по крайней мере, они находятся в одном и том же месте, поэтому их легко изменить.

app.locales используется в контроллере по умолчанию (requirements={"_locale" = "%app.locales%"}), а locale_supported используется в LocaleRewriteListener. Если он обнаружит языковой стандарт, которого нет в списке, он вернется к языковому стандарту по умолчанию, который в данном случае является значением locale:en.

app.locales хорош с командой требований, потому что она вызовет 404 для любых локалей, которые не совпадают.

Если вы используете формы и у вас есть логин, вам нужно будет сделать следующее с вашим security.yml

Security.yml

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost: 12
        AppBundle\Entity\User:
            algorithm: bcrypt
            cost: 12

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
         database:
              entity: { class: AppBundle:User }
                #property: username
                # if you're using multiple entity managers
                # manager_name: customer

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/
            anonymous: true

            form_login:
                check_path: login_check
                login_path: login_route
                provider: database
                csrf_token_generator: security.csrf.token_manager

            remember_me:
                secret:   '%secret%'
                lifetime: 604800 # 1 week in seconds
                path:     /
                httponly: false
                #httponly false does make this vulnerable in XSS attack, but I will make sure that is not possible.
            logout:
                path:   /logout
                target: /

    access_control:
        # require ROLE_ADMIN for /admin*
        #- { path: ^/login, roles: ROLE_ADMIN }
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/(.*?)/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_USER }

Важное изменение, на которое следует обратить внимание, заключается в том, что (.*?)/login будет проходить аутентификацию анонимно, чтобы ваши пользователи могли по-прежнему входить в систему. Это означает, что такие маршруты, как ..dogdoghere/login, могут срабатывать, но требования, которые я покажу вам в ближайшее время для маршрутов входа, предотвращают это и выдают ошибку 404. Мне нравится это решение с (.*?) по сравнению с [a-z]{2}, если вы хотите использовать локали типа en_US.

SecurityController.php

<?php
// src/AppBundle/Controller/SecurityController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class SecurityController extends Controller
{
    /**
     * @Route("{_locale}/login", name="login_route", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
     */
    public function loginAction(Request $request)
    {
        $authenticationUtils = $this->get('security.authentication_utils');

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();

        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render(
            'security/login.html.twig',
            array(
                // last username entered by the user
                'last_username' => $lastUsername,
                'error'         => $error,
            )
        );
    }

    /**
     * @Route("/{_locale}/login_check", name="login_check", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
     */
    public function loginCheckAction()
    {
        // this controller will not be executed,
        // as the route is handled by the Security system
    }

    /**
    * @Route("/logout", name="logout")
    */
    public function logoutAction()
    {
    }
}
?>

Обратите внимание, что даже эти пути используют {_locale} впереди. Однако мне это нравится, поэтому я могу создавать собственные логины для разных языков. Просто имейте это в виду. Единственный маршрут, для которого не нужна локаль, — это выход из системы, который отлично работает, поскольку на самом деле это всего лишь маршрут перехвата для системы безопасности. Также обратите внимание, что он использует требования, установленные в config.yml, поэтому вам нужно только отредактировать его в одном месте для всех маршрутов в ваших проектах.

Надеюсь, это поможет кому-то, пытающемуся сделать то, что я делал!

ПРИМЕЧАНИЕ.: Чтобы легко это проверить, я использую расширение «Quick Language Switcher» для Google Chrome, которое изменяет заголовок accept-language для всех запросов.

person Joseph Astrahan    schedule 11.01.2016
comment
Ваш контроллер также может иметь префикс @Route, прямо перед открытием class {}. Вы можете попробовать установить туда свой _locale и посмотреть, работает ли он, чтобы вам не приходилось повторять его в каждом действии. - person Artamiel; 11.01.2016
comment
artamiel, ваша идея сработала везде, кроме контроллера безопасности, потому что маршрут выхода изменяется, но если бы я дал маршруту выхода имя, это, вероятно, сработало бы. Я попробую это, и если это сработает, обновите мой ответ, чтобы все было немного чище. - person Joseph Astrahan; 13.01.2016
comment
Рад слышать это. В любом случае вам нужно определить 2 дополнительных маршрута — logout и login, так что даже если они не имеют автоматических префиксов, это не такая уж большая проблема, если остальные ваши маршруты настроены правильно. - person Artamiel; 13.01.2016
comment
Да, вы правы, это также вариант, чтобы выйти из системы в своей области, или, поскольку в этом маршрутизаторе их всего 2, просто сделайте их исключениями. - person Joseph Astrahan; 14.01.2016
comment
Как насчет использования $request->getDefaultLocale($this->supportedLocales) в LocaleRewriteListener.php? Тогда нам не нужен `` isLocaleSupported(...)`. - person simohe; 14.01.2020
comment
и работает ли $request->getPreferredLanguage(); до запуска LocaleListener ? - person simohe; 14.01.2020

конечная функция smallResumeOfResearching($localeRewrite, $opinion = 'IMHO') :)

  1. Метод, предоставленный г-ном. Джозеф прекрасно работает с такими маршрутами, как /{route_name} или /, но не с такими маршрутами, как /article/slug/other.

  2. Если мы используем модифицированный метод мистера Джозефа, предоставленный https://stackoverflow.com/a/37168304/9451542, мы потеряет профилировщик и отладчик в режиме разработки.

  3. Если нам нужно более гибкое решение, метод onKernelRequest можно изменить следующим образом (спасибо г-ну Джозефу, благодаря https://stackoverflow.com/a/37168304/9451542):

    public function onKernelRequest(GetResponseEvent $event)
    {
        $pathInfo = $event->getRequest()->getPathinfo();
        $baseUrl = $event->getRequest()->getBaseUrl();
        $checkLocale = explode('/', ltrim($pathInfo, '/'))[0];
    
        //Or some other logic to detect/provide locale
    
        if (($this->isLocaleSupported($checkLocale) == false) && ($this->defaultLocale !== $checkLocale)) {
            if ($this->isProfilerRoute($checkLocale) == false) {
                $locale = $this->defaultLocale;
                $event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
        }
        /* Or with matcher:
        try {
             //Try to match the path with the locale prefix
             $this->matcher->match('/' . $locale . $pathInfo);
             //$event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
        } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
        } catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
        }
        */
        }
    }
    

    примечание: $this->profilerRoutes = array('_profiler', '_wdt', '_error');

  4. Спасибо Susana Santos за указание на простой метод настройки :)
person Vitalijus Trainys    schedule 19.04.2018

Небольшое улучшение для Symfony 3.4:

  1. Убедитесь, что getSubscribedEvents() зарегистрирует LocaleRewriteListener ПЕРЕД RouterListener::onKernelRequest и ПЕРЕД LocaleListener::onKernelRequest. Целое число 17 должно быть больше приоритета RouterListener::onKernelRequest. В противном случае вы получите 404.

    bin/консоль отладки:диспетчер событий

  2. Определение службы в services.yml должно быть (зависит от конфигурации Symfony):

    AppBundle\EventListener\LocaleRewriteListener: аргументы: ['@router', '%kernel.default_locale%', '%locale_supported%'] теги: - { name: kernel.event_subscriber, event: kernel.request }

person Vitalijus Trainys    schedule 17.04.2018
comment
Спасибо за помощь будущим посетителям этой темы с обновлением - person Joseph Astrahan; 17.04.2018