symfony 2 set locale based on user preferences stored in DB - php

I am trying to set the locale based on the current user's preferences which are stored in the DB.
Our User class therefore has a getPreferredLanguage which returns a locale identify ('en', 'fr_FR', etc.).
I've considered the following approach:
register a "locale" listener service that subscribes to the KernelEvents::REQUEST event.
this service has access to the security context (via its constructor)
this service's onKernelRequest method attempts to get the user from the security context, get the user's preferred locale, and set it as the request's locale.
Unfortunately, this doesn't work. When the "locale" listener service's onRequestEvent method is invoked, the security context does not have a token. It seems that the context listener is invoked at a very late stage (with a priority of 0), and it is impossible to tell my "locale" listener to run before the security context.
Does anyone know how to fix this approach, or suggest another one?

You may be interested in the locale listener, which I posted in this answer: Symfony2 locale detection: not considering _locale in session
Edit: If a user changes his language in the profile, it's no problem. You can hook into profile edit success event if you're are using FOSUserBundle (master). Otherwise in your profile controller, if you're using a self made system. Here is a example for FOSUserBundle:
<?php
namespace Acme\UserBundle\EventListener;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ChangeLanguageListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::PROFILE_EDIT_SUCCESS => 'onProfileEditSuccess',
);
}
public function onProfileEditSuccess(FormEvent $event)
{
$request = $event->getRequest();
$session = $request->getSession();
$form = $event->getForm();
$user = $form->getData();
$lang = $user->getLanguage();
$session->set('_locale', $lang);
$request->setLocale($lang);
}
}
and in the services.yml
services:
acme.change_language:
class: Acme\UserBundle\EventListener\ChangeLanguageListener
tags:
- { name: kernel.event_subscriber }
for multiple sessions in multiple browser is no problem, as every new session requires a new login. Hmm, ok, not after changing the language, as only the current session would be updated. But you can modify the LanguageListener to support this.
And the case if an admin changes the language should be insignificant.

If you reach this answer through Google, I am currently using this solution.
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
class SetLocaleEventSubscriber implements EventSubscriberInterface
{
private Security $security;
private TranslatorInterface $translator;
public function __construct(Security $security, TranslatorInterface $translator)
{
$this->security = $security;
$this->translator = $translator;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [
['setLocale', 1]
]
];
}
public function setLocale(ControllerEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
/**
* #var \App\Entit\User
*/
$user = $this->security->getUser();
if ($user) {;
$request->setLocale($user->getLocale());
$this->translator->setLocale($user->getLocale());
}
}
}

In order to achieve this, you need to setup an event subscriber on the Kernel::REQUEST event with a higher priority than the default Locale listener as indicated in the documentation
At this time, you will unfortunately not be able to access to the current logged in user because this is something set in another Symfony event triggered after the Locale listener.
However, you can access to the session.
The solution is to save the user's locale in the session just after a successful login, and then set the locale in the request from the session.
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Stores the locale of the user in the session after the
* login. This can be used by the LocaleSubscriber afterwards.
*/
class UserLocaleSubscriber implements EventSubscriberInterface
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->requestStack->getSession()->set('_locale', $user->getLocale());
}
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
];
}
}
// src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if ($request->getSession()) {
// Set user's locale from session
if ($locale = $request->getSession()->get('_locale')) {
$request->setLocale($locale);
}
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}

Related

Symfony 4.3 + ApiPlatform + Accept-Language

I have just started with Api Platform and I have a problem with translating validator's messages.
I've:
...created two files with translations: /translations/validators.en.yaml and /translations/validators.fr.yaml
...added apropriate custom validator message
...set default_locale as en
...added translation validator definition in Entity:
/**
* #ORM\Column(type="text", nullable=true)
* #Assert\NotBlank(message="test.message")
*/
private $title;
After that my custom translated message is returned correctly.
Now I want to enable translation relying on Accept-Language header instead of default_locale.
I know that symfony does not relay on Accept-Language header. Instead of that I created subscriber which is responsible for determining preferred language (from Accept-Language):
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class LocaleSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$locale = substr($request->getPreferredLanguage(), 0, 2);
$request->setDefaultLocale($locale);
$request->setLocale($locale);
}
public static function getSubscribedEvents()
{
return [
'kernel.request' => 'onKernelRequest',
];
}
}
Unfortunatelly this way doesn't work. Translation still relay on kernel.default_locale even if I put 'fr' directly as parameter to setLocale and setDefaultLocale.
Does anybody know how to resolve my problem?
I agree with the pervious answer, but there is an other problem: Browsers use a dash in locales, but Symfony uses underscores. Here is a complete solution:
// https://github.com/metaclass-nl/tutorial-api-platform/blob/chapter4-api/api/src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$accept_language = $request->headers->get("accept-language");
if (empty($accept_language)) {
return;
}
$arr = HeaderUtils::split($accept_language, ',;');
if (empty($arr[0][0])) {
return;
}
// Symfony expects underscore instead of dash in locale
$locale = str_replace('-', '_', $arr[0][0]);
$request->setLocale($locale);
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}
Take a look at this Symfony documentation page:
https://symfony.com/doc/current/session/locale_sticky_session.html
I think your listener should have a higher priority (like 20) so it is loaded before the local listener.

FOSUserBundle - Event set variable in session when success authentication?

I would like to know if it is possible to insert a variable in the session if authentication succeds (using with FOSUserBundle).
It is more or less than two lines to insert.
$session = $request->getSession();
$this->$session->set('type','OneType');
Is there a very simple way to do it? I really want to do it when there is successful authentication, not anywhere.
You need to listen to the event security.interactive_login. (Docs)
Simple example, using an event subscriber:
<?php
// src/EventSubscriber/SecuritySubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\SecurityEvents;
class SecuritySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'successfulLogin',
];
}
public function successfulLogin( InteractiveLoginEvent $event )
{
$event->getRequest()->getSession()->set('foo', 'bar');
}
}
You haven't specified version, but this should work on a default installation for Symfony 4.

Laravel: How to set globally available default route parameters

I'm trying to set a handful of default route parameters that will work globally in my application regardless of context. In the documentation for URL generation the example given is using middleware which is fine for HTTP, but won't get called during non-HTTP contexts. I also need this to work when called from the CLI.
My first idea is to have a Service Provider that calls the defaults method on boot:
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaults extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults([
'foo' => 'abc',
'bar' => 'xyz',
]);
}
}
But this does not work for HTTP requests:
Route::get('test', function (\Illuminate\Routing\UrlGenerator $urlGenerator) {
dump($urlGenerator->getDefaultParameters());
});
Outputs []
I believe this is because in the UrlGenerator, the setRequest method unconditionally sets the routeGenerator property to null. My Service Provider's boot method is called during the bootstrapping process, but then the request is set afterwards clobbering my defaults.
//Illuminate/Routing/UrlGenerator.php
public function setRequest(Request $request)
{
$this->request = $request;
$this->cachedRoot = null;
$this->cachedSchema = null;
$this->routeGenerator = null;
}
Dumping the UrlGenerator during boot and then again in my routes file can demonstrate this:
As you can see, the UrlGenerator instance is the same both times, but the RouteUrlGenerator on the routeGenerator property has changed.
I am unsure of a better way to set these defaults.
Not sure why this is getting attention almost a year later, but I ended up finding a solution by myself.
To add a bit more information to the original question, the purpose of this was to allow us to have the same instance of the code powering both our live and sandbox application. There's more involved to get this working, but this issue was just about URL generation for links in views. All links generated always both a subdomain and tld, so this code injects these values always.
These views are rendered both as a response to a HTTP request, e.g. in our client areas, but also as part of a non HTTP request, e.g. a scheduled task generating invoices and emailing them to clients.
Anyway, the solution:
For non HTTP contexts, a service provider can set the defaults:
<?php namespace App\Providers;
use App\Support\UrlDefaults;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaultsServiceProvider extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults(UrlDefaults::getDefaults());
}
}
Since the there's no routing going on to cause the problem I asked originally, this just works.
For HTTP contexts, the RouteMatched event is listened for and the defaults injected then:
<?php namespace App\Listeners;
use App\Support\UrlDefaults;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
/**
* Class SetUrlDefaults
*
* This class listeners for the RouteMatched event, and when it fires, injects the route paramaters (subdomain, tld,
* etc) into the defaults of the UrlGenerator
*
* #package App\Listeners
*/
class SetUrlDefaults
{
private $urlGenerator;
private $router;
public function __construct(UrlGenerator $urlGenerator, Router $router)
{
$this->urlGenerator = $urlGenerator;
$this->router = $router;
}
public function handle(): void
{
$paramaters = array_merge(UrlDefaults::getDefaults(), $this->router->current()->parameters);
$this->urlGenerator->defaults($paramaters);
}
}
UrlDefaults is just a simple class that returns an array:
<?php namespace App\Support;
class UrlDefaults
{
public static function getDefaults(): array
{
return [
'tld' => config('app.url.tld'),
'api' => config('app.url.api'),
'foo' => config('app.url.foo'),
'bar' => config('app.url.bar'),
];
}
}
So digging into the source for routing classes a bit more, there’s a defaults() method on the UrlGenerator class, but it’s not a singleton, so any defaults you set in a service provider aren’t persisted.
I seem to have got it working by setting the defaults in some middleware:
Route::domain('{domain}')->middleware('route.domain')->group(function () {
//
});
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
class SetRouteDomain
{
private $url;
public function __construct(UrlGenerator $url)
{
$this->url = $url;
}
public function handle($request, Closure $next)
{
$this->url->defaults([
'domain' => $request->getHost(),
]);
return $next($request);
}
}

Symfony4 SwitchUserSubscriber doesn't distinguish between two users

in my S4 application i need the switch user "functionality" for super user. I have a form input with autocomplete to search users (available only for ROLE_SUPERADMIN) but i would like to forbid to a user to impersonate itself. I've implemented this eventsubscriber but for both , $currentUser and $targetUser, it return the same identity (the target one). Where am i wrong?
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
class SwitchUserSubscriber implements EventSubscriberInterface
{
public function onSecuritySwitchUser(SwitchUserEvent $event)
{
$currentUser = $event->getToken()->getUser();
$targetUser = $event->getTargetUser();
if($currentUser->getUsername() == $targetUser->getUsername()){
throw new UnsupportedUserException("You can't impersonate yourself");
}
}
public static function getSubscribedEvents()
{
return [
'security.switch_user' => 'onSecuritySwitchUser',
];
}
}
Is there a better way to achieve this?
Ok found the solution, read the code comments for explanation
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Core\Role\SwitchUserRole;
class SwitchUserSubscriber implements EventSubscriberInterface
{
/**
* #param SwitchUserEvent $event
*/
public function onSecuritySwitchUser(SwitchUserEvent $event)
{
// Current user initialized to null
$currentUser = null;
// Gets all the roles in switching phase
$roles = $event->getToken()->getRoles();
// Cycles between roles
foreach($roles as $role) {
// SwitchUserRole it's a role of the impersonating user
// The if statement doesn't happen in switch exit
if ($role instanceof SwitchUserRole) {
// Recupera l'uente
$currentUser = $role->getSource()->getUser();
}
}
// Impersonated user
$targetUser = $event->getTargetUser();
// If you wann be yourself raises an exception
if(null !== $currentUser && ($currentUser->getUsername() == $targetUser->getUsername())){
throw new UnsupportedUserException("You can't impersnate yourself");
}
}
public static function getSubscribedEvents()
{
return [
'security.switch_user' => 'onSecuritySwitchUser',
];
}
}

Symfony 3 - Can't pass token_storage to from subscriber

I want to get current logged user in form event but for some reason I can't get it to work.
I used services to inject token_storage and create constructor method to fetch token storage instance but I got error right at constructor:
Type error: Argument 1 passed to AdminBundle\Form\EventListener\CompanyFieldSubscriber::__construct() must implement interface Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface, none given
I am not sure what is the problem and how to fix it. Does someone knows where is the problem?
EDIT 1:
I think that I found out where is the problem but I can't find "good" solution. I call this event in form type in this way:
->addEventSubscriber(new CompanyFieldSubscriber());
Problem is that I am not using 'service/dependency injection' to create event and I am sending nothing to constructor. That's why I have this error (not 100% sure to be hones).
Since I have around 20-30 forms and new forms will come in time I need to create service for each form that requires user (or token_storage) instance and as a argument call token_storage or this event subscriber as a argument of service.
I know that it will work if I create each form as a service and pass required data as arguments but is there way to process this "automatically" without creating new service for every form that needs to have some user data interaction in form events?
EDIT 2:
As suggested I tried to change event subscriber constructor but I got same error with different class name.
New code:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
New error:
Type error: Argument 1 passed to AdminBundle\Form\EventListener\CompanyFieldSubscriber::__construct() must be an instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage, none given
This is code I am using:
Services:
admin.form.event_listener.company:
class: AdminBundle\Form\EventListener\CompanyFieldSubscriber
arguments: ['#security.token_storage']
tags:
- { name: form.event_listener }
Event Listener:
namespace AdminBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class CompanyFieldSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
private $user;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
$this->user = $this->tokenStorage->getToken()->getUser();
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmitData',
];
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
if (in_array("ROLE_SUPER_ADMIN", $this->user->getRoles())) {
$form->add('company', EntityType::class, [
'class' => 'AppBundle:Company',
'choice_label' => 'name'
]);
}
}
public function preSubmitData(FormEvent $event)
{
$form = $event->getForm();
$bus = $form->getData();
if (!in_array("ROLE_SUPER_ADMIN", $this->user->getRoles())) {
$bus->setCompany($this->user->getCompany());
}
}
}
You call subscriber in wrong way when you use:
new CompanyFieldSubscriber()
You do not pass TokenStorageInterface to subscriber constructor. Call it as a service, if it is in controller then:
->addEventSubscriber($this->get('admin.form.event_listener.company'));
if it is in form than pass from controller to form
$this->get('admin.form.event_listener.company')
as option and then use it in form
for Symfony >= 4
use Symfony\Component\Security\Core\Security;
class ExampleService
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function someMethod()
{
$user = $this->security->getUser();
}
}
See doc: https://symfony.com/doc/current/security.html#fetching-the-user-from-a-service

Categories