Symfony 3 将所有路由重定向到当前语言环境版本

Posted

技术标签:

【中文标题】Symfony 3 将所有路由重定向到当前语言环境版本【英文标题】:Symfony 3 Redirect All Routes To Current Locale Version 【发布时间】:2016-04-15 03:28:46 【问题描述】:

我正在开发一个 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 Use default locale in routing (one URL for one language)

Symfony2 default locale in routing

::更新::

我还没有解决我的问题,但我已经接近并学会了一些提高效率的技巧。

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 例如不会将用户重定向到 /browsers locale/admin,这将是保持一切井井有条的更优雅的解决方案......但至少路由工作.仍在寻找更好的解决方案。

****更新****

我想我可能在这里找到了答案,作为给出的底部答案 (Add locale and requirements to all routes - Symfony2),Athlan 的答案。只是不确定如何在 symfony 3 中实现这一点,因为他的指示对我来说不够清楚。

我认为这篇文章也可能会有所帮助 (http://symfony.com/doc/current/components/event_dispatcher/introduction.html)

【问题讨论】:

【参考方案1】:

我没有足够的声誉来为正确的解决方案添加评论。所以我要添加一个新答案

您可以像这样在 app/config/routing.yml 添加“前缀:/_locale”:

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

因此,您无需将其添加到每个操作的每个路由中。对于以下步骤。非常感谢它是完美的。

【讨论】:

你的声望已经足够了 :)。【参考方案2】:

经过 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 很适合 requirements 命令,因为它会导致任何不匹配的语言环境出现 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 设置的要求,因此您只需在一个地方为您的项目中的所有路由编辑它。

希望这对尝试做我正在做的事情的人有所帮助!

注意:: 为了轻松测试这一点,我使用 Google Chrome 的“快速语言切换器”扩展程序,它会更改所有请求的接受语言标头。

【讨论】:

您的控制器也可以在class 开头之前添加@Route 注释前缀。您可以尝试在其中设置您的_locale,看看它是否有效,这样您就不必在每个操作中重复它。 artamiel 你的想法在除安全控制器之外的所有地方都有效,因为注销路由被修改了,但如果我给注销路由命名,它可能会起作用。我会试试这个,如果它有效,请更新我的答案,这样事情会更干净一些。 很高兴听到这个消息。在任何情况下,您都需要定义 2 个额外的路由 - logoutlogin,因此即使它们没有自动添加前缀,如果您的其余路由配置正确,也没什么大不了的。 是的,你是对的,这也是一个选项,在它自己的区域进行注销,或者因为它在那个路由器中只有 2 个,所以只是让那些例外。 在 LocaleRewriteListener.php 中使用$request-&gt;getDefaultLocale($this-&gt;supportedLocales) 怎么样?那么我们就不需要`isLocaleSupported(...)`了。【参考方案3】:

最终函数 smallResumeOfResearching($localeRewrite, $opinion = '恕我直言') :)

    方法,由先生提供。 Joseph 在 /route_name 或 / 之类的路线上表现出色,但在 /article/slug/other 之类的路线上表现不佳。

    如果我们使用 https://***.com/a/37168304/9451542 提供的修改后的 mr.Joseph 方法,我们将在开发模式下丢失分析器和调试器。

    如果我们想要更灵活的解决方案,可以这样修改 onKernelRequest 方法(感谢 Joseph 先生,感谢https://***.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');

    感谢 Susana Santos 指出简单的配置方法 :)

【讨论】:

【参考方案4】:

Symfony 3.4 的小改进:

    请确保 getSubscribedEvents() 将在 RouterListener::onKernelRequest 和 LocaleListener::onKernelRequest 之前注册 LocaleRewriteListener。整数 17 必须大于 RouterListener::onKernelRequest 优先级。否则你会得到 404。

    bin/console debug:event-dispatcher

    services.yml 中的服务定义必须是(取决于 Symfony 配置):

    AppBundle\EventListener\LocaleRewriteListener: 参数:['@router', '%kernel.default_locale%', '%locale_supported%'] 标签: - 名称:kernel.event_subscriber,事件:kernel.request

【讨论】:

感谢您帮助未来访问此线程的访问者进行更新

以上是关于Symfony 3 将所有路由重定向到当前语言环境版本的主要内容,如果未能解决你的问题,请参考以下文章

index.php到哪个路由重定向?

在 symfony 中重定向到“/”路由

Symfony 内核创建 302 重定向而不是使用正确的路由

从 Symfony2 中的 routing.yml 将一条路由重定向(301)到另一条路由

Symfony 安全重定向到登录页面

Laravel 在区域设置更改时重定向到路由