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
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']
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.
I'm working on a Symfony (2.7.4) website where some users have their own language resources for their own locales. For example, a user might have two locales (for example fr and en) and another user could also have one or all of these locales.
Each user has the ability to edit its own translations in a third party app, so translations are not shared between users.
I would like to be able to load the appropriate (YML or XLIFF) resources file when accessing a user's page, based on the locale (which is defined in the URL) and the user (could be its ID or anything that identifies it).
For example, when visiting user99.my-domain.ext/fr/ I'd like to add [base_directory]/user99/messages.fr.yml to the resources loaded by the Translator so it overrides the keys in the base messages.fr.yml.
I've tried to inject the Translator in my service, but I can only use it for reading translations, not adding any. What would be the best way to do that? Or is doing it in a service is too late? Maybe the Kernel is a better place?
Any help is appreciated!
Note: I'm using the YAML format in my examples, but any of the Symfony-known formats is eligible.
In order to "override" translations you should decorate the translator service. That way your own translator logic will be executed when trans() is called somewhere.
http://symfony.com/doc/current/components/dependency_injection/advanced.html#decorating-services
By decorating the service other bundles will also start using the logic you did describe above. You can inject the active user (eg. token_storage service) and some caching services (eg. https://github.com/doctrine/DoctrineCacheBundle) to make sure your user gets the right translations.
This isn't related to the request or hostname, your translation logic for the user should happen after the firewall / authorization logic was executed.
See Symfony's LoggingTranslator PR to find out how the decorator pattern was used to let the translator log missing translations: https://github.com/symfony/symfony/pull/10887/files
I chose to use a custom Twig filter, so I can decide when I want user specific translations and when I want generic ones.
Here's my extension (my users are in fact Domain instances):
<?php
// src/AppBundle/Twig/DomainTranslationExtension
namespace AppBundle\Twig;
use AppBundle\Document\Domain;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;
class DomainTranslationExtension extends \Twig_Extension {
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var Translator
*/
protected $translator;
/**
* #var string
*/
protected $locale;
/**
* #var Domain
*/
protected $domain;
public function setContainer(ContainerInterface $container) {
$this->container = $container;
$this->locale = $this->container->get('request_stack')->getMasterRequest()->getLocale();
$domainService = $this->container->get('app.domain_service');
$this->domain = $domainService->getDomain();
// TODO: File loading error check
$this->translator = new Translator($this->locale);
$this->translator->addLoader('yaml', new YamlFileLoader());
$this->translator->addResource('yaml', 'path/to/domain-translations/' . $this->domain->getSlug() . '/messages.' . $this->locale . '.yml', $this->locale);
}
public function getFilters() {
return array(
new \Twig_SimpleFilter('transDomain', array($this, 'transDomain')),
);
}
public function transDomain($s) {
$trans = $this->translator->trans($s);
// Falling back to default translation if custom isn't available
if ($trans == $s) {
$trans = $this->container->get('translator')->trans($s);
}
return $trans;
}
public function getName() {
return 'app_translation_extension';
}
}
Declared like that in app/config/services.yml:
app.domain_service:
class: AppBundle\Services\DomainService
arguments: [ #request_stack, #doctrine_mongodb, #translation.loader ]
And used like that in a Twig file:
{{ 'my.translation.key'|transDomain }}
I hope this helps, thanks!
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
}
}