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.
Related
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);
}
}
Symfony uses nonces in the development web toolbar like this :
<div id="sfwdtd61de8" class="sf-toolbar sf-display-none"></div><script
nonce=ca6666b27bc9c402c16192e4b43bbdaa>
etc and then, since the nonces are dynamically generated, i can't use in my vhost this kind of code for Content Security Policy :
Header set Content-Security-Policy script-src 'self' 'nonce-
ca6666b27bc9c402c16192e4b43bbdaa'
So what am i supposed to do in order to whitelist the web developer toolbar code ?
I'm using :
Symfony 3.3.2
Apache 2.4.25
PHP 7.1.2
Create a subscriber:
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
class NonceSubscriber implements EventSubscriberInterface
{
private $tokenGenerator;
public function __construct( TokenGeneratorInterface $tokenGenerator )
{
$this->tokenGenerator = $tokenGenerator;
}
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
$token_CSRF = $this->tokenGenerator->generateToken();
$response->headers->set
(
'Content-Security-Policy', "script-src nonce-".$token_CSRF."';"
);
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
}
After this you will see your nonce $token_CSRF together with the nonce generated by the WebProfilerBundle in the response headers.
For a project I am required to have a persistent session for a visitor.
A couple of years ago I faced the issue with an Apple update temporary rendering all iPhones unable to set PHPSESSID cookies.
I created a fall back method which checked for the SESSION ID in the URL and use that to persist the session between requests. I am aware of the fact this can be enabled in php.ini using the session.use_trans_sid.
Point is I do not want this to happen always. When possible I prefer the cookie method.
Is there a way within Symfony to add this logic to the route methods adding the session identifier?
Can anyone help me to explain where to extend the twig "path" method to add the logic to optionally append the session id to all URL's generated by that method.
UPDATE
Let me post an update on my progress and perhaps someone can help me. I managed to find how to extend the UrlGenerator with my own code by replacing the generator_base_class in a parameter.
Now I have the following issue.
I wish to use a session to do some logic. I however can not reach this core component as a service. I already tried makign a compilerPass for both the UrlGenerator and an extended Router class to be able to make a dependency injection in one of these classes.
However until now it sadly failed.
What would be the best partice to get the Session component within the UrlGenerator class?
I was able to create my solution thanks to this post:
Override router and add parameter to specific routes (before path/url used)
In the end this is the code I came up with.
In my service.xml
<parameters>
<parameter key="router.class">Acme\CoreBundle\Component\Routing\Router</parameter>
<parameter key="router.options.generator_base_class">Acme\CoreBundle\Component\Routing\Generator\UrlGenerator</parameter>
</parameters>
Extending Symfony's core router to make in ContainerAware and force that container to the UrlGenerator.
namespace Acme\CoreBundle\Component\Routing;
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RequestContext;
class Router extends BaseRouter implements ContainerAwareInterface
{
private $container;
public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null)
{
parent::__construct($container, $resource, $options, $context);
$this->setContainer($container);
}
public function getGenerator()
{
$generator = parent::getGenerator();
$generator->setContainer($this->container);
return $generator;
}
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
}
Extending the UrlGenerator class.
namespace Acme\CoreBundle\Component\Routing\Generator;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* UrlGenerator generates URL based on a set of routes, this class extends the basics from Symfony.
*/
class UrlGenerator extends BaseUrlGenerator implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
{
/** #var \Symfony\Component\HttpFoundation\Session\Session $session */
$session = $this->container->get('session');
if (true !== $session->get('acceptCookies')) {
$parameters[$session->getName()] = $session->getId();
}
return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
}
In the end this results in the session name and id being appended to the generated URL when the session value acceptCookies is not equal to true.
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]],
];
}
}
I have a Symfony2 project and I am using Translation component for translating text. I have all translations in yml file like so
translation-identifier: Translated text here
Translating text looks like this from Twig
'translation-identifier'|trans({}, 'domain')
The thing is, in some cases I would like to have two different texts for same translation (not for pluralization). Here's how I would like it to work:
Define two texts in yml file for translations that need to have different texts. Each would have it's own unique suffix
translation-identifier-suffix1
translation-identifier-suffix2
Define a global rule that would define which suffix should be choosen. Psuedocode below:
public function getSuffix() {
return rand(0, 10) < 5 ? '-suffix1' : '-suffix2';
}
Twig (and PHP) would look the same - I would still specify just the identifier without suffix. Translator would then append suffix to the identifier and try to find a match. If there would be no match it would try to find a match again without suffix.
AFAIK, Translator component doesn't support it.
But if you want same kind of behavior, you could do by overriding the translator service.
1) Override the service
# app/config/config.yml
parameters:
translator.class: Acme\HelloBundle\Translation\Translator
First, you can set the parameter holding the service's class name to your own class by setting it in app/config/config.yml.
FYI: https://github.com/symfony/FrameworkBundle/blob/master/Resources/config/translation.xml
2) Extend the translator class provided symfony framework bundle.
FYI: https://github.com/symfony/FrameworkBundle/blob/master/Translation/Translator.php
3) Overwrite the trans function which is provider by translator component.
https://github.com/symfony/Translation/blob/master/Translator.php
Hope this helps!
Here is the extended translator class in case anyone ever needs it
<?php
namespace Acme\HelloBundle\Translation;
use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Translator extends BaseTranslator {
const SUFFIX_1 = '_suffix1';
const SUFFIX_2 = '_suffix2';
private $suffix;
public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array()) {
parent::__construct($container, $selector, $loaderIds, $options);
$this->suffix = $this->getSuffix($container);
}
public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null) {
if ($locale === null)
$locale = $this->getLocale();
if (!isset($this->catalogues[$locale]))
$this->loadCatalogue($locale);
if($this->suffix !== null && $this->catalogues[$locale]->has((string) ($id . $this->suffix), $domain))
$id .= $this->suffix;
return strtr($this->catalogues[$locale]->get((string) $id, $domain), $parameters);
}
private function getSuffix($container) {
return rand(0, 10) < 5 ? self::SUFFIX_1 : self::SUFFIX_2;
}
}
?>
As of Symfony 3, Venu's answer no longer works completely, as the translator.class parameter is no longer used.
To load your custom translator class, you now need to create a compiler pass.
<?php
namespace Acme\HelloBundle\DependencyInjection\Compiler;
use Acme\HelloBundle\Translation\Translator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class TranslatorOverridePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->getDefinition('translator.default')->setClass(Translator::class);
}
}
And this compiler pass needs to be added to the container.
<?php
namespace Acme\HelloBundle;
use Acme\HelloBundle\DependencyInjection\Compiler\TranslatorOverridePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeHelloBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new TranslatorOverridePass());
}
}