Symfony 3将所有路由重定向到当前语言环境版本 [英] Symfony 3 Redirect All Routes To Current Locale Version

查看:139
本文介绍了Symfony 3将所有路由重定向到当前语言环境版本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发一个symfony应用程序,无论用户位于哪个页面上,我的目标都是导航到该页面的语言环境版本.

I am working on a symfony application where my goal is no matter what page the user is on it will navigate to the locale version of the page.

例如,如果用户导航到"/"主页,它将重定向到"/en/"

For example, if the user navigates to "/" the home page, it will redirect to "/en/"

如果它们位于"/admin"页面上,它将重定向到"/en/admin" ,以这种方式在路由中设置_locale属性.

If they are on "/admin" page it will redirect to "/en/admin", in such a way that the _locale property is set from the route.

由于还没有确定语言环境,因此还需要确定他们是否从用户浏览器访问/admin的语言环境,因此它知道要重定向到的页面.

Also it needs to determine the locale if they visit /admin from the users browser since no locale was determined so it knows which page to redirect to.

由于我正在测试,目前我的默认控制器如下所示.我正在使用开发模式&分析器以测试翻译是否正确.

Currently my default controller looks like below since I'm testing. I'm using the dev mode & profiler to test that translations are working in correct.

<?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)

This current method will keep the user at "/" if they navigate there, but I want to have it redirect to "/en/". This should work for other pages too, like /admin, or /somepath/pathagain/article1 (/en/admin , /en/somepath/pathagain/article1)

我该怎么做?

我读过的参考资料没有帮助:

References I've read that did not help:

Symfony2在路由中使用默认语言环境(一个网址代表一种语言)

Symfony2路由中的默认语言环境

:: Update ::

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

I have not solved my issue but I've come close as well as learned a few tricks to be more efficient.

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%"是引用config参数的部分.

Notice under parameters the value app.locales: en|es|zh. This is now a value I can reference whenever I create my routes if I plan to support more locales in the future which I do. Those routes are english, spanish, chinese in that order for those curious. In the DefaultController in the annotations the "%app.locales%" is the part that references the config parameter.

例如,我当前方法的问题是去/admin不会将用户重定向到/{browsers locale}/admin,这将是使一切保持井井有条的更优雅的解决方案...但是至少路由可以正常工作.仍在寻找更好的解决方案.

The problem with my current method is going to /admin for example does not redirect the user to /{browsers locale}/admin, which would be the more elegant solution to keep everything organized... but at least the routes work. Still looking for better solution.

****更新****

****Update****

我认为我可能已经在这里找到了答案,这是给出的最基本的答案(

I think I may have possibly found the answer here as the bottom answer given (Add locale and requirements to all routes - Symfony2), the answer by Athlan. Just not sure how to implement this in symfony 3 as his directions were not clear enough to me.

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

I think this article might help also (http://symfony.com/doc/current/components/event_dispatcher/introduction.html)

推荐答案

经过12个小时的研究,我终于找到了可以接受的解决方案.如果可以提高效率,请发布此解决方案的修订版.

After 12 hours of looking into this I finally found an acceptable solution. Please post revised versions of this solution if you can make it more efficient.

需要注意的一些事情,我的解决方案特别适合我的需要.它的作用是强制任何URL转到本地化版本(如果存在).

Some things to note, my solution is particular to my need. What it does is force any URL to go to a localized version if it exists.

在创建路由时,这需要遵循一些约定.

This requires some conventions to be followed when you create routes.

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}/"开头.为了使它起作用,项目中的每条路线都需要具有此功能.之后,您只需输入真实的路线名称即可.对我来说,我对这种情况还可以.您可以轻松地修改我的解决方案以满足您的需求.

Notice that both routes always start with "/{_locale}/". For this to work every route in your project needs to have this. You just put the real route name afterwards. For me I was okay with this scenario. You can modify my solution to fit your needs easily enough.

第一步是在httpKernal上创建一个侦听器,以在请求进入路由器进行渲染之前拦截请求.

The first step is to create a listen on the httpKernal to intercept requests before they go to the routers to render them.

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以启动侦听器.

Finally you set the services.yml to start the listener up.

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中的参数下添加以下内容:

Also in the config.yml you will want to add the following under parameters:

config.yml

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

我希望只有一个地方可以定义区域设置,但是我不得不做2个...但是至少它们位于同一位置,所以很容易更改.

I wanted there to be only one place you define the locales but I wound up having to do 2...but at least they are in the same spot so easy to change.

app.locales,在LocaleRewriteListener中使用locale_supported.如果它检测到不在列表中的语言环境,则会回退到默认语言环境,在这种情况下,该语言环境是locale:en的值.

app.locales is used in default controller (requirements={"_locale" = "%app.locales%"}) and locale_supported is used in the LocaleRewriteListener. If it detects a locale that is not in the list it will fallback to the default locale, which in this case is the value of locale:en.

app.locales可与Requirements命令配合使用,因为它会导致任何不匹配的语言环境产生404错误.

app.locales is nice with the requirements command because it will cause a 404 for any locales that do not match.

如果您正在使用表单并登录,则需要对security.yml执行以下操作

If you are using forms and have a login you will need to do the following to your 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错误.如果您想使用en_US类型的语言环境,我喜欢将(.*?)[a-z]{2}相对比的解决方案.

The important change to note here is that (.*?)/login will authenticate anonymously so your users can still login. This does mean that routes like..dogdoghere/login could trigger, but the requirements I will show you shortly on the login routes prevent this and will throw 404 errors. I like this solution with the (.*?) versus [a-z]{2} incase you wanted to use en_US type locales.

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中设置的要求,因此您只需要针对项目中所有路线在一个地方对其进行编辑.

Note that even these paths use {_locale} in front. I like this however so I can give custom logins for different locales. Just keep that in mind. The only route that does not need the locale is logout which works just fine since its really only an intercept route for the security system. Also notice it uses the requirements which is set from the config.yml, so you only have to edit it in one place for all the routes across your projects.

希望这可以帮助某人尝试做我正在做的事情!

Hope this helps someone trying to do what I was doing!

注意::为了便于测试,我使用了Google Chrome的快速语言切换器"扩展程序,该扩展程序更改了所有请求的接受语言标头.

NOTE:: To test this easily I use 'Quick Language Switcher' extension for Google Chrome, which changes the accept-language header on all requests.

这篇关于Symfony 3将所有路由重定向到当前语言环境版本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆