Symfony 3 перенаправляет все маршруты в текущую версию локали
Я работаю над приложением Symfony, где моя цель не зависит от того, на какой странице находится пользователь, и перейдет к языковой версии страницы.
Например, если пользователь переходит на "/" домашнюю страницу, он будет перенаправлен на "/ en /"
Если они находятся на странице "/ admin", она будет перенаправлена на "/ en / admin" таким образом, что _locale
свойство устанавливается из маршрута.
Также он должен определить локаль, если они посещают / admin из браузера пользователя, поскольку локаль не была определена, поэтому он знает, на какую страницу перенаправить.
В настоящее время мой контроллер по умолчанию выглядит следующим образом, так как я тестирую. Я использую dev mode & profiler для проверки правильности работы переводов.
<?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 не перенаправляет пользователя в / {locales locale} / admin, что было бы более элегантным решением, чтобы все было организовано... но, по крайней мере, маршруты работают. Все еще ищу лучшее решение.
****Обновить****
Я думаю, что, возможно, я нашел ответ здесь как нижний ответ ( Добавить локаль и требования ко всем маршрутам - Symfony2), ответ Атлана. Просто не уверен, как реализовать это в Symfony 3, так как его указания были недостаточно ясны для меня.
Я думаю, что эта статья также может помочь ( http://symfony.com/doc/current/components/event_dispatcher/introduction.html)
4 ответа
После 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. Если он обнаруживает локаль, которой нет в списке, он возвращается к локали по умолчанию, которая в этом случае является значением локали: en.
app.locales хорош с командой require, потому что она вызовет 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 на все запросы.
У меня недостаточно репутации, чтобы добавить комментарий к правильному решению. Поэтому я добавляю новый ответ
Вы можете добавить "префикс: /{_locale}" в app/config/routing.yml следующим образом:
app:
resource: "@AppBundle/Controller/"
type: annotation
prefix: /{_locale}
Поэтому вам не нужно добавлять его в каждый маршрут к каждому действию. Для следующих шагов. Спасибо большое, это прекрасно.
Конечная функция smallResumeOfResearching($localeRewrite, $ мнение = 'ИМХО'):)
Метод, предоставленный г-ном. Джозеф отлично работает с такими маршрутами, как /{route_name} или /, но не с такими маршрутами, как / article / slug / other.
Если мы используем модифицированный метод mr. Joseph, предоставленный /questions/46464769/yazyik-po-umolchaniyu-symfony2-v-marshrutizatsii/46464781#46464781, мы потеряем профилировщик и отладчик в режиме разработки.
Если мы хотим более гибкое решение, метод onKernelRequest можно изменить следующим образом (спасибо г-ну Джозефу, спасибо /questions/46464769/yazyik-po-umolchaniyu-symfony2-v-marshrutizatsii/46464781#46464781):
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');
- Спасибо Сусане Сантос за указание на простой метод настройки:)
Небольшое улучшение для Symfony 3.4:
Убедитесь, что getSubscribeedEvents() зарегистрирует LocaleRewriteListener ПЕРЕД RouterListener::onKernelRequest и ПЕРЕД LocaleListener::onKernelRequest. Целое число 17 должно быть больше, чем Priotity RouterListener::onKernelRequest. В противном случае вы получите 404.
Отладка бина / консоли: диспетчер событий
Определение сервиса в services.yml должно быть (зависит от конфигурации Symfony):
AppBundle \ EventListener \ LocaleRewriteListener: аргументы: ['@router', '%kernel.default_locale%', '%locale_supported%'] теги: - {имя: kernel.event_subscriber, событие: kernel.request }