How can I change the locale using symfony 3.4 (php)?
I have the locale saved for each user in my database. Now on this page they explain that you should create an event listener to set the locale. But they only provide a method - in which class do I put this method and how do I wire it up using my services.yml?
And if I'm in the service - how can I access my user object to actually get the locale I want to set?
Here is an example provided by the docs on how to create an kernel request listener.
In this listener you inject the TokenStorage serivce, which then provides you the current token and with it the attached user that is currently logged in. Then you take the locale from the user and set it to the request.
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class RequestListener
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
$user = $this->tokenStorage->getToken()->getUser();
$request = $event->getRequest();
$request->setLocale($user->getLocale());
}
}
To understand, why Symfony requires the type-hint of an interface instead of an class please read the documentation.
Related
I tried to follow the information in the Symfony docs to make a user local sticky during the users session by using a UserLocaleSubscriber.
The example code from the docs uses the InteractiveLoginEvent which seems to be deprecated in Symfony 6 and LoginSuccessEvent should be used instead. No matter which event I use in my code (both events are fired when using the login form), the local
class UserLocaleSubscriber implements EventSubscriberInterface {
private $requestStack;
private $logger;
public function __construct(RequestStack $requestStack, LoggerInterface $logger) {
$this->requestStack = $requestStack;
$this->logger = $logger;
}
public static function getSubscribedEvents(): array {
return [
//SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
LoginSuccessEvent::class => 'onLoginSuccess'
];
}
public function onInteractiveLogin(InteractiveLoginEvent $event): void {
...
}
public function onLoginSuccess(LoginSuccessEvent $event) {
$user = $event->getAuthenticatedToken()->getUser();
$locale = $user->getLocale();
$this->logger->info("onLoginSuccess: $locale");
if (null !== $locale) {
$this->logger->info(" set");
$this->requestStack->getSession()->set('_locale', $locale);
$event->getRequest()->setLocale($locale);
}
}
}
// Log
onLoginSuccess: de [] {"url":"/login",...}
set [] {"url":"/login",...}
UserLocaleSubscriber [] {"url":"/restrictedPage",...}
So, the event subscribers are correctly called when handling the login page and the locale is set to the session. However, on the next request when redirecting to the restricted page, no event is fired, and thus the locale is not applied. So here the default fallback locale is used. But shouldn't be the locale correctly in the session at this point?
Ok, I found the answer. I will leave the question in place in case someone else stumbles over the same problem:
In fact the code from the question is correct and does add the local to the session. However, the translator does NOT load the locale from the session but from the current request. So the locale is stored correctly but not used...
To solve this, one has to add a request listener which checks the session for a locale and add it to the request. This is described in the docs linked in the question. I got this wrong and though one has to implement one OR the other when in fact both listeners are needed.
I am trying to tidy up my session variables by integrating custom AttributBags into the session. In Symfony < 6.0 you were able to inject a custom AttributBag into the session service.
See related questions
How to add extra bag to symfony session
Using Symfony AttributeBags in a Controller
However this approach does not work anymore in Symfony >= 6.0. This blog article explains that the session service is deprecated and must now be accessed over the request_stack service. For controllers this works fine.
My current (not working) approach looks like this: Define a custom AttributBag class.
class ShoppingCartBag extends AttributeBag {
public function __construct(string $storageKey = 'shoppingCart') {
parent::__construct($storageKey);
}
}
Add a custom CompilerPass in the Kernel class so that Symfony takes care of all changes while building the container.
class Kernel extends BaseKernel {
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void {
$container->addCompilerPass(new AddShoppingCartBagToSessionService());
}
}
The custom CompilerPass looks like this.
class AddShoppingCartBagToSessionService implements CompilerPassInterface {
public function process(ContainerBuilder $container) {
$container->getDefinition('request_stack') //<- Works, but how to access the session?
->addMethodCall('getSession') // How to bridge the gap? This thought does not work. I assume it is because the session is not yet instantiated when the container is build.
->addMethodCall('registerBag', [new Reference('App\Session\CustomBag\ShoppingCartBag')]);
}
}
As you correctly assumed, the session does not exist yet when doing this via the compiler pass.
Symfony uses a so called SessionFactory to create the session. So what you can do instead, is decorating the existing session.factory service with your own implementation of the SessionFactoryInterface and add your attribute bag there:
An implementation of this decorated session factory might look like this:
namespace App;
use Symfony\Component\HttpFoundation\Session\SessionFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class SessionFactoryWithAttributeBag implements SessionFactoryInterface
{
public function __construct(private SessionFactoryInterface $delegate)
{
}
public function createSession(): SessionInterface
{
$session = $this->delegate->createSession();
$session->registerBag(new ShoppingCartBag());
return $session;
}
}
And then you can decorate the session.factory via the services.yaml:
services:
App\SessionFactoryWithAttributeBag:
decorates: session.factory
arguments: ['#.inner']
Now, whenever a session is created, your custom bag is also registered
That was an important clue, thank you #Spea!
I adopted his idea and created a new decorator for the session service. After some trial and error I found an answer to my problem. The solution looks like this. Notice the actual syntax is slightly different from the answer given by Spea.
Create a custom AttributBag by extending the likewise named class. Be careful to set the name of the attribut bag, not the storage key in constructor. Otherwise Symfony will throw an error when you try to access the ShoppingCartBag.
namepsace App\Session;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
class ShoppingCartBag extends AttributeBag {
public function __construct() {
parent::__construct();
$this->setName('shoppingCart');
}
}
Create a decorator to change the session service's behaviour to get the desired result (include the ShoppingCartBag on each session).
namespace App\Decorator;
use App\Session\ShoppingCartBag;
use Symfony\Component\HttpFoundation\Session\SessionFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class SessionFactoryShoppingCartBag implements SessionFactoryInterface {
public function __construct(private SessionFactoryInterface $delegate) {}
public function createSession(): SessionInterface {
$session = $this->delegate->createSession();
$session->registerBag(new ShoppingCartBag());
return $session;
}
}
Then decorate the session service in the services.yml by adding the following piece of code.
services:
App\Decorator\SessionFactoryShoppingCartBag:
decorates: session.factory
arguments: ['#.inner']
I am using symfony 3.4 and I want to set the display locale for a request.
I tried as explained in the Symfony documentation with the following code
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class EventListener
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$request->setLocale('en');
}
}
but it doesn't affect the page. On the symfony status bar it still shows me de_DE as locale. I read the explanation on this stackoverflow post, but I feel like I did exactly what is described there. So what did I miss?
The custom listener must be called before LocaleListener, which initializes the locale based on the current request. To do so, set your listener priority to a higher value than LocaleListener priority (which you can obtain running the debug:event kernel.request command).
check the documentation https://symfony.com/doc/3.4/translation/locale.html
I am running a Symfony 2.8 based webpage which uses the FOSUserBundle. When the user switches from the public part of the webpage to the private part by logging in, the session is invalided (PHPSESSID changes). Thus after logging in it it not possible any more to access the session which was used on the public part.
In the Symfony docs I found information about the invalidate_session in the logout config.
While it makes sense to clean the session data when logging out, I do not understand what's the reason to the same when logging in.
Question 1:
Is there an option to prevent Symfony from invalidating the session when logging in?
Even if there an option to change this behavior I would preferr to keep it this way (to prevent any unforeseen side effects). This brings us to the second question:
Question 2:
Is there any event or other way that can be used to access the public session before it gets invalidated during the login process?
The Firewall.php uses an onKernelRequest handler with priority 8 to run its authentication methods. Thus I tried to use my on own onKernelRequest handler with a higher priority to access the session first, but this did not work out. I get only access to the new session.
How to solve this?
You should implement an EventSubscriber and subscribe to the events
SecurityEvents::INTERACTIVE_LOGIN and FOSUserEvents::REGISTRATION_COMPLETED. At that point the public session is not yet invalidated and you can get the user from the event.
namespace AppBundle\EventListener;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FilterUserResponseEvent;
class YourCustomListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onUserAuthentication',
FOSUserEvents::REGISTRATION_COMPLETED => ['onUserRegistration', -10]
];
}
public function onUserAuthentication(InteractiveLoginEvent $event): void
{
$user = $event->getAuthenticationToken()->getUser();
$this->yourFuntionUsingTheSessionHere($user);
}
public function onUserRegistration(FilterUserResponseEvent $event): void
{
$user = $event->getUser();
$this->yourFunctionUsingTheSessionHere($user);
}
private function yourFunctionUsingTheSessionHere(User $user): void
{
// do your thing here
// I don't know if Eventsubscribers are containeraware maybe you need to inject the container or Symfony\Component\HttpFoundation\Session\SessionInterface to have access to the session
}
}
I'm using the FOS user bundle for symfony2 and want to run some custom code to log the event when a user confirms his registration at /register/confirm/{token}
However, there does not seem to be an event for when a user is confirmed, so I'd like to know the best way to hook into execution when a user account is confirmed.
You can override RegistrationController (see doc) to add logging functionality.
If you're able to use the dev-master (2.0.*#dev), than you can use the new controller events in the FOSUserBundle. See the documentation at github for details: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/controller_events.md
Here is a little example for the confirm event. Don't forget to define the service as mentioned in the link above.
<?php
namespace Acme\UserBundle\Security\Listener;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class RegistrationListener implements EventSubscriberInterface
{
public function __construct(/** inject services you need **/)
{
// assign services to private fields
}
public static function getSubscribedEvents()
{
return array(FOSUserEvents::REGISTRATION_CONFIRM => 'onRegistrationConfirm',);
}
/**
* GetResponseUserEvent gives you access to the user object
**/
public function onRegistrationConfirm(GetResponseUserEvent $event)
{
// do your stuff
}
}