My remember function for my symfony application seems to be not working appropriately. I've followed the resource provided by Symfony itself here.
Anyway, here is part of my security.yaml file:
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
form_login:
login_path: /p/login
entry_point: form_login
# login_throttling:
# limiter: app.custom.limiter
lazy: true
provider: app_user_provider
# https://symfony.com/doc/current/security/impersonating_user.html
switch_user: true
custom_authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
# where to redirect after logout
# target: app_any_route
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
token_provider:
doctrine: true
My LoginFormAuthenticator.php file:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\RateLimiter\RateLimiterFactory;
class LoginFormAuthenticator extends AbstractAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordHasherInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request): ?bool
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function authenticate(Request $request): Passport
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$csrfToken = $request->request->get('_csrf_token');
return new Passport(
new UserBadge($email),
new PasswordCredentials($password),
[
new CsrfTokenBadge('authenticate', $csrfToken),
new RememberMeBadge()
],
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Invalid credentials.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('feed', ['page_num' => 1]));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->urlGenerator->generate('login'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
The database schema that was created by using the doctrine as the token provider:
The user gets logged in but then logs out automatically after an hour (I believe) which has gotten annoying. Even though 1 week has been specified in the security.yaml file.
UPDATE
After reading threads via github (here), it seems this approach isn't viable. What alternative should we use to allow our users to stay logged in within the specified duration that we note in our security.yaml file?
Consider that if we use cookies or regular sessions for user log ins, every user will be logged out every time we deploy because of the cache:clear function.
Goto user entity and find getUserIdentyfier() function. Make sure this returns email.
Property: email<
I'm building a custom GuardAuthenticator to login with a token on a specific route. According to the documentation if supportsRememberMe() returns true and remember_me is activated in the firewall, the remember me cookie should be set, but it's not (although it is set if I use a form login authentication on another route).
The route:
/**
* #Route("/login/token/{id}/{token}/{force}", defaults={"force"=0}, name="login_token")
*/
public function loginToken()
{
}
The GuardAuthenticator:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class TokenLoginAuthenticator extends AbstractGuardAuthenticator
{
use TargetPathTrait;
private $em;
private $force;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function supports(Request $request)
{
return 'login_token' === $request->attributes->get('_route') && $request->isMethod('GET');
}
public function getCredentials(Request $request)
{
$credentials = [
'id' => $request->attributes->get('id'),
'token' => $request->attributes->get('token')
];
$this->force = $request->attributes->get('force');
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->em->getRepository(User::class)->find($credentials['id']);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('No user found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user->getToken() === $credentials['token']) {
return true;
}
throw new HttpException(403, "Forbidden");
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
throw new HttpException(403, "Forbidden");
}
public function start(Request $request, AuthenticationException $authException = null)
{
}
public function supportsRememberMe()
{
return true;
}
}
The security config:
security:
encoders:
App\Entity\User:
id: 'App\Security\PasswordEncoder'
providers:
in_memory: { memory: ~ }
orm:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
form_login:
login_path: login
check_path: login
provider: orm
csrf_token_generator: security.csrf.token_manager
default_target_path: homepage
logout:
path: /logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
# by default, the feature is enablered by checking a
# checkbox in the login form (see below), uncomment the
# following line to always enable it.
# always_remember_me: true
guard:
provider: orm
authenticators:
- App\Security\TokenLoginAuthenticator
In Symfony 5.2.6 dont forget to add new RememberMeBadge() to the authenticate function:
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
new RememberMeBadge(),
]
);
}
}
Remember me cookie will be set if all of the following are met:
The supportsRememberMe() method returns true.
The remember_me key in the firewall is configured.
The (default) _remember_me parameter is sent in the request. This is usually done by having a _remember_me checkbox in a login form (but it can be sent as url param (?_remember_me=1), or we can configure the firewall remember_me key to always_remember_me.
The onAuthenticationSuccess() method returns a Response object.
This question already has an answer here:
Symfony & Guard: "The security token was removed due to an AccountStatusException"
(1 answer)
Closed 5 years ago.
I try to make a form login authentication with guard (symfony 3.2) but it doesn't work.
The authentication is working, but when I'm redirected to the home page (accueil), I'm redirected to the login page without anthentication.
If I put in the controler of my home page
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
I can see my user, the role but he is not authenticated.
DashboardController.php on line 23:
PostAuthenticationGuardToken {#133 ▼
-providerKey: "main"
-user: User {#457 ▶}
-roles: array:1 [▼
0 => Role {#120 ▼
-role: "ROLE_SUPERADMIN"
}
]
-authenticated: false
-attributes: []
}
What I've missed ?
Security.ym
security:
encoders:
EntBundle\Entity\User\User:
algorithm: bcrypt
providers:
database:
entity:
class: EntBundle:User\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
logout: ~
guard:
authenticators:
- ent.login_authenticator
TestAuthenticator.php
namespace EntBundle\Security;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class TestAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $router;
public function __construct(EntityManager $em, RouterInterface $router)
{
$this->em = $em;
$this->router = $router;
}
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
return $this->em->getRepository('EntBundle:User\User')->findOneBy(['username' => $username]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// this is just for test
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('accueil');
return new RedirectResponse($url);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function supportsRememberMe()
{
return false;
}
}
DashboardController.php
namespace EntBundle\Controller\Dashboard;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DashboardController extends Controller
{
/**
* #Route("/accueil", name="accueil")
*/
public function indexAction()
{
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
return $this->render('EntBundle:dashboard:dashboard_structure.html.twig');
}
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('EntBundle::login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
EDIT:
Thanks leo_ap for your help but the problem doesnt come from there.
The config session is like this :
session:
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
and if I check in the save path folder I have session file created but not authenticated.
_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:29:"http://localhost:8000/accueil";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1488245179;s:1:"c";i:1488244922;s:1:"l";s:1:"0";}
If I try the normal login_form with security.yml it's working fine...
I've try with handler_id and save_path at null with no success.
EDIT2:
I've found why I'm always redirected to the login page, because I'm logged out!
[2017-02-28 09:16:34] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0): at /home/philippe/Documents/symfony/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"}
and in GuardAuthenticationProvider.php (86)
The listener *only* passes PreAuthenticationGuardToken instances.
This means that an authenticated token (e.g.PostAuthenticationGuardToken)
is being passed here, which happens if that token becomes "not authenticated" (e.g. happens if the user changes between requests).
In this case, the user should be logged out, so we will return an AnonymousToken to accomplish that.
But Why ???
May be your Session that isn't persisting the token. Check your Session configuration, inside: config.yml. in the framework option, there is session. See how the handler_id and save_path are configured. It may be that your php instalation is unable to handle the sessions on the configured path. Try to put null to handler_id and save_path to force php use its own build in configurations to handle sessions.
config.yml file:
framework:
{ .. Other configurations ..}
session:
handler_id: null
save_path: null
{ .. More configurations ..}
Good day, everyone.
I need to extend authentication mechanism for my needs.
To do this i created Custom Form Password Authenticator
1) I changed firewall settings
main:
...
#organization-form-login:
simple_form:
authenticator: my_authenticator
csrf_provider: form.csrf_provider
check_path: oro_user_security_check
login_path: oro_user_security_login
...
2) I created service for my_authenticator
services:
...
my_authenticator:
class: OQ\SecurityBundle\Security\MyAuthenticator
arguments:
- #oro_organization.organization_manager
...
3) And here is the code of MyAuthenticator
namespace OQ\SecurityBundle\Security;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
Use Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationToken;
use Oro\Bundle\OrganizationBundle\Entity\Manager\OrganizationManager;
class MyAuthenticator implements SimpleFormAuthenticatorInterface
{
/** #var OrganizationManager */
protected $manager;
public function __construct(OrganizationManager $manager)
{
$this->manager = $manager;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
// Here will be my special checks
//Here i try to get username and force authentication
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('This user not allowed');
}
// If everythin' is ok - create a token
if ($user) {
return new UsernamePasswordOrganizationToken(
$user,
$user->getPassword(),
$providerKey,
$this->manager->getOrganizationById(1)
);
} else {
throw new AuthenticationException('Invalid username or password');
}
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordOrganizationToken
&& $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
//UsernamePasswordOrganizationToken
return new UsernamePasswordOrganizationToken($username, $password, $providerKey, $this->manager->getOrganizationById(1));
}
}
When i try to authenticate user - i succesfully log in, but i dont see anything except black header and profiler. Profiler says me, that i'm logged as USER_NAME (yellow color), and not authenticated (red color).
Can you give me an advice - how to make t work?
And one more question - how can i retrieve user's organization in this authenticator class?
If you check UsernamePasswordToken constructor you'll see it requires you to pass $roles in order to make it authenticated
parent::setAuthenticated(count($roles) > 0);
And it's impossible to change authenticate flag after in setAuthenticated (see the code why).
Check also UserAuthenticationProvider class to get an idea what's happening.
I hope this helps.
I have a problem with routing and the internationalization of my site built with Symfony2.
If I define routes in the routing.yml file, like this:
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
It works fine with URLs like:
mysite.com/en/example
mysite.com/fr/example
But doesn't work with
mysite.com/example
Could it be that optional placeholders are permitted only at the end of an URL ?
If yes, what could be a possible solution for displaying an url like :
mysite.com/example
in a default language or redirecting the user to :
mysite.com/defaultlanguage/example
when he visits :
mysite.com/example. ?
I'm trying to figure it out but without success so far.
Thanks.
If someone is interested in, I succeeded to put a prefix on my routing.yml without using other bundles.
So now, thoses URLs work :
www.example.com/
www.example.com//home/
www.example.com/fr/home/
www.example.com/en/home/
Edit your app/config/routing.yml:
ex_example:
resource: "#ExExampleBundle/Resources/config/routing.yml"
prefix: /{_locale}
requirements:
_locale: |fr|en # put a pipe "|" first
Then, in you app/config/parameters.yml, you have to set up a locale
parameters:
locale: en
With this, people can access to your website without enter a specific locale.
You can define multiple patterns like this:
example_default:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index}
requirements:
_locale: fr|en
You should be able to achieve the same sort of thing with annotations:
/**
* #Route("/example", defaults={"_locale"="fr"})
* #Route("/{_locale}/example", requirements={"_locale" = "fr|en"})
*/
Hope that helps!
This is what I use for automatic locale detection and redirection, it works well and doesn't require lengthy routing annotations:
routing.yml
The locale route handles the website's root and then every other controller action is prepended with the locale.
locale:
path: /
defaults: { _controller: AppCoreBundle:Core:locale }
main:
resource: "#AppCoreBundle/Controller"
prefix: /{_locale}
type: annotation
requirements:
_locale: en|fr
CoreController.php
This detects the user's language and redirects to the route of your choice. I use home as a default as that it the most common case.
public function localeAction($route = 'home', $parameters = array())
{
$this->getRequest()->setLocale($this->getRequest()->getPreferredLanguage(array('en', 'fr')));
return $this->redirect($this->generateUrl($route, $parameters));
}
Then, the route annotations can simply be:
/**
* #Route("/", name="home")
*/
public function indexAction(Request $request)
{
// Do stuff
}
Twig
The localeAction can be used to allow the user to change the locale without navigating away from the current page:
{{ targetLanguage }}
Clean & simple!
The Joseph Astrahan's solution of LocalRewriteListener works except for route with params because of $routePath == "/{_locale}".$path)
Ex : $routePath = "/{_locale}/my/route/{foo}" is different of $path = "/{_locale}/my/route/bar"
I had to use UrlMatcher (link to Symfony 2.7 api doc) for matching the actual route with the url.
I change the isLocaleSupported for using browser local code (ex : fr -> fr_FR). I use the browser locale as key and the route locale as value. I have an array like this array(['fr_FR'] => ['fr'], ['en_GB'] => 'en'...) (see the parameters file below for more information)
The changes :
Check if the local given in request is suported. If not, use the default locale.
Try to match the path with the app route collection. If not do nothing (the app throw a 404 if route doesn't exist). If yes, redirect with the right locale in route param.
Here is my code. Works for any route with or without param. This add the locale only when {_local} is set in the route.
Routing file (in my case, the one in app/config)
app:
resource: "#AppBundle/Resources/config/routing.yml"
prefix: /{_locale}/
requirements:
_locale: '%app.locales%'
defaults: { _locale: %locale%}
The parameter in app/config/parameters.yml file
locale: fr
app.locales: fr|gb|it|es
locale_supported:
fr_FR: fr
en_GB: gb
it_IT: it
es_ES: es
services.yml
app.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
LocaleRewriteListener.php
<?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;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var urlMatcher \Symfony\Component\Routing\Matcher\UrlMatcher;
*/
private $urlMatcher;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'fr', array $supportedLocales, $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
$context = new RequestContext("/");
$this->matcher = new UrlMatcher($this->routeCollection, $context);
}
public function isLocaleSupported($locale)
{
return array_key_exists($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivalent when exists.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
// Do nothing if the route requested has no locale param
$request = $event->getRequest();
$baseUrl = $request->getBaseUrl();
$path = $request->getPathInfo();
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
if ($this->isLocaleSupported($locale)) {
$locale = $this->supportedLocales[$locale];
} else if ($locale == ""){
$locale = $request->getDefaultLocale();
}
$pathLocale = "/".$locale.$path;
//We have to catch the ResourceNotFoundException
try {
//Try to match the path with the local prefix
$this->matcher->match($pathLocale);
$event->setResponse(new RedirectResponse($baseUrl.$pathLocale));
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
}
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Symfony3
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
requirements:
_locale: en|bg| # put a pipe "|" last
There is my Solution, it makes this process faster!
Controller:
/**
* #Route("/change/locale/{current}/{locale}/", name="locale_change")
*/
public function setLocaleAction($current, $locale)
{
$this->get('request')->setLocale($locale);
$referer = str_replace($current,$locale,$this->getRequest()->headers->get('referer'));
return $this->redirect($referer);
}
Twig:
<li {% if app.request.locale == language.locale %} class="selected" {% endif %}>
{{ language.locale }}
</li>
I have a full solution to this that I discovered after some research. My solution assumes that you want every route to have a locale in front of it, even login. This is modified to support Symfony 3, but I believe it will still work in 2.
This version also assumes you want to use the browsers locale as the default locale if they go to a route like /admin, but if they go to /en/admin it will know to use en locale. This is the case for example #2 below.
So for example:
1. User Navigates To -> "/" -> (redirects) -> "/en/"
2. User Navigates To -> "/admin" -> (redirects) -> "/en/admin"
3. User Navigates To -> "/en/admin" -> (no redirects) -> "/en/admin"
In all scenarios the locale will be set correctly how you want it for use throughout your program.
You can view the full solution below which includes how to make it work with login and security, otherwise the Short Version will probably work for you:
Full Version
Symfony 3 Redirect All Routes To Current Locale Version
Short Version
To make it so that case #2 in my examples is possible you need to do so using a httpKernal listner
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)),
);
}
}
To understand how that is working look up the event subscriber interface on symfony documentation.
To activate the listner you need to set it up in your 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 }
Finally this refers to variables that need to be defined in your config.yml
config.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
locale_supported: ['en','es','zh']
Finally, you need to make sure all your routes start with /{locale} for now on. A sample of this is below in my default controller.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/{_locale}", requirements={"_locale" = "%app.locales%"})
*/
class DefaultController extends Controller
{
/**
* #Route("/", name="home")
*/
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")
*/
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
]);
}
}
?>
Note the requirements requirements={"_locale" = "%app.locales%"}, this is referencing the config.yml file so you only have to define those requirements in one place for all routes.
Hope this helps someone :)
We created a custom RoutingLoader that adds a localized version to all routes. You inject an array of additional locales ['de', 'fr'] and the Loader adds a route for each additional locale. The main advantage is, that for your default locale, the routes stay the same and no redirect is needed. Another advantage is, that the additionalRoutes are injected, so they can be configured differently for multiple clients/environments, etc. And much less configuration.
partial_data GET ANY ANY /partial/{code}
partial_data.de GET ANY ANY /de/partial/{code}
partial_data.fr GET ANY ANY /fr/partial/{code}
Here is the loader:
<?php
namespace App\Routing;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class I18nRoutingLoader extends Loader
{
const NAME = 'i18n_annotation';
private $projectDir;
private $additionalLocales = [];
public function __construct(string $projectDir, array $additionalLocales)
{
$this->projectDir = $projectDir;
$this->additionalLocales = $additionalLocales;
}
public function load($resource, $type = null)
{
$collection = new RouteCollection();
// Import directly for Symfony < v4
// $originalCollection = $this->import($resource, 'annotation')
$originalCollection = $this->getOriginalRouteCollection($resource);
$collection->addCollection($originalCollection);
foreach ($this->additionalLocales as $locale) {
$this->addI18nRouteCollection($collection, $originalCollection, $locale);
}
return $collection;
}
public function supports($resource, $type = null)
{
return self::NAME === $type;
}
private function getOriginalRouteCollection(string $resource): RouteCollection
{
$resource = realpath(sprintf('%s/src/Controller/%s', $this->projectDir, $resource));
$type = 'annotation';
return $this->import($resource, $type);
}
private function addI18nRouteCollection(RouteCollection $collection, RouteCollection $definedRoutes, string $locale): void
{
foreach ($definedRoutes as $name => $route) {
$collection->add(
$this->getI18nRouteName($name, $locale),
$this->getI18nRoute($route, $name, $locale)
);
}
}
private function getI18nRoute(Route $route, string $name, string $locale): Route
{
$i18nRoute = clone $route;
return $i18nRoute
->setDefault('_locale', $locale)
->setDefault('_canonical_route', $name)
->setPath(sprintf('/%s%s', $locale, $i18nRoute->getPath()));
}
private function getI18nRouteName(string $name, string $locale): string
{
return sprintf('%s.%s', $name, $locale);
}
}
Service definition (SF4)
App\Routing\I18nRoutingLoader:
arguments:
$additionalLocales: "%additional_locales%"
tags: ['routing.loader']
Routing definition
frontend:
resource: ../../src/Controller/Frontend/
type: i18n_annotation #localized routes are added
api:
resource: ../../src/Controller/Api/
type: annotation #default loader, no routes are added
I use annotations, and i will do
/**
* #Route("/{_locale}/example", defaults={"_locale"=""})
* #Route("/example", defaults={"_locale"="en"}, , requirements = {"_locale" = "fr|en|uk"})
*/
But for yml way, try some equivalent...
Maybe I solved this in a reasonably simple way:
example:
path: '/{_locale}{_S}example'
defaults: { _controller: 'AppBundle:Example:index' , _locale="de" , _S: "/" }
requirements:
_S: "/?"
_locale: '|de|en|fr'
Curious about the judgement of the critics ...
Best wishes,
Greg
root:
pattern: /
defaults:
_controller: FrameworkBundle:Redirect:urlRedirect
path: /en
permanent: true
How to configure a redirect to another route without a custom controller
I think you could simply add a route like this:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index }
This way, the locale would be the last locale selected by the user, or the default locale if user locale has not been set. You might also add the "_locale" parameter to the "defaults" in your routing config if you want to set a specific locale for /example:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
I don't know if there's a better way to do this.