Does anybody know how to set the locale in Symfony2.1?
I am trying with:
$this->get('session')->set('_locale', 'en_US');
and
$this->get('request')->setLocale('en_US');
but none of those has any effect, the devbar tells me:
Session Attributes: No session attributes
Anyway, it is always the fallback locale that is used, as defined in config.yml
(PS: I am trying to set up the translation system as described here
Even though the Symfony 2.1 states that you can simply set the locale via the Request or Session objects, I never managed to have it working, setting the locale simply has no effect.
So I ended up using a listener coupled with twig routing to handle the locale/language:
The listener:
namespace FK\MyWebsiteBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
static public function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Register the listener in service.xml:
<service id="fk.my.listener" class="FK\MyWebsiteBundle\Listener\LocaleListener">
<argument>%locale%</argument>
<tag name="kernel.event_subscriber"/>
</service>
The routing must look like:
homepage:
pattern: /{_locale}
defaults: { _controller: FKMyWebsiteBundle:Default:index, _locale: en }
requirements:
_locale: en|fr|zh
And handle the routing with:
{% for locale in ['en', 'fr', 'zh'] %}
<a href="{{ path(app.request.get('_route'), app.request.get('_route_params')|merge({'_locale' : locale})) }}">
{% endfor %}
This way, the locale will automatically be set when you click on a link to change the language.
You set the locale in your parameters.yml.
[parameters]
...
locale = en
The fallback from your config.yml references %locale% which is the setting from the above parameters.yml file.
If you are trying to set it on-the-fly then this should work:
$this->get('session')->setLocale('en_US');
Test it by printing it out straight after:
print_r($this->get('session')->getLocale());
Edit
In 2.1 the locale is now stored in the request but can still be set in the session. http://symfony.com/doc/2.1/book/translation.html#handling-the-user-s-locale
$this->get('session')->set('_locale', 'en_US');
// setting via request with get and setLocale
$request = $this->getRequest();
$locale = $request->getLocale();
$request->setLocale('en_US');
It's not:
$this->get('request')->setLocale('en_US');
But:
$this->get('request')->getSession()->set('_locale', 'en_US');
From the symfony cookbook:
"Locale is stored in the Request, which means that it's not "sticky" during a user's request. In this article, you'll learn how to make the locale of a user "sticky" so that once it's set, that same locale will be used for every subsequent request."
http://symfony.com/doc/current/cookbook/session/locale_sticky_session.html
You can notice this when you set the locale and use the symfony profiler (in dev mode) to view the sub requests.
Related
I am using rollbar.com to collect all details about exceptions in symfony2 app. However I don't understand how can I configure monolog so it would pass username and user id to rollbar.
I see that I can pass rollbar config as shown here and I am thinking person_fn is what I need. Still I don't know where to put this function (this should be in service because I need to check security token) and how to pass it to rollbar.
# config_prod.yml
rollbar:
type: rollbar
level: error
token: %rollbar_token%
config:
person_fn: getUserForRollbarRightAboutNowOrSomething
Found solution:
update monolog/monolog bundle to at least 1.17.0 version.
create ContextProcessor and update user information
#src/AppBundle/Monolog/RollbarContextProcessor
namespace AppBundle\Monolog;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class RollbarContextProcessor
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function processRecord($record)
{
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
if ($user instanceof User) {
$record['context']['payload']['person'] = [
'id' => $user->getId(),
'username' => $user->getUsername(),
'email' => $user->getEmail(),
];
}
}
return $record;
}
}
configure ContextProcessor as service with monolog.processor tag.
# app/config/config_prod.yml
services:
monolog.processor.rollbar_context:
class: AppBundle\Monolog\RollbarContextProcessor
arguments: [#security.token_storage]
tags:
- { name: monolog.processor, method: processRecord, handler: rollbar }
monolog:
handlers:
rollbar:
type: rollbar
level: error
token: %rollbar_token%
Your question has two parts:
Rollbar
person_fn is exactly what you need. You should be able to add a reference to the function by using a string (e.g.: "MyClass::static_function_reference" or "my_function_name").
Symfony
Disclaimer: I don't use or know much about Symfony.
This question has some excellent examples of how to get the current user in Symfony. (Punch line: in a controller you can call $this.getUser())
This question has a good example of how to inject the current user in a service. (Make a Twig Extension that depends on the SecurityContext or TokenStorage, use those dependencies to get a user objet).
Finally, there's the classic PHP move: as soon as you have a user add it to $_REQUEST. I'm not sure if Symfony co-opts this, but it'd be a valid way in a non-framework PHP application.
I'm working on security for an API. Some things that I check on each request are:
Is the user's IP address whitelisted for access
Is the user's account expired
Is the user's rate limit exceeded for the day?
It seems like I should use a security Voter, perhaps one for each of these things, and return VoterInterface::ACCESS_DENIED when an access check fails.
However, I want to provide a message to the user in the API response that provides some indication as to WHY their request was denied. I cannot do this with a security voter.
My current workaround is to listen to the kernel controller event, perform my access checks, and then throw an explicit AccessDeniedException with my specific message if the check fails.
Is that a good way to handle this shortcoming? Maybe there's a way to do this within the security voter that I'm overlooking?
I know this is an old post but I just had the same problem and found a solution that works like a charm.
PS: I'm using symfony 3.4.4
The idea is to pass the RequestStack to the voter (constructor injection) then get the session and add a message in the flashBag to be displayed after that.
The voter constructor:
use Symfony\Component\HttpFoundation\RequestStack;
class ContactVoter extends Voter {
private $requestStack;
function __construct(RequestStack $requestStack) {
$this->requestStack= $requestStack;
}
If you are not using autowire and want to pass it as an argument in the service.yml you can use arguments: [#request_stack]
Inside the voter, the function that decide the permissions:
if ( 'Your_Access_Denied_Condition') {
$this->requestStack->getCurrentRequest()->getSession()->getFlashBag()->add('danger', 'Your message !');
return false ;
}
The template to display the message
{% if app.request.hasPreviousSession %}
{% for type, messages in app.session.flashbag.all() %}
{% for message in messages %}
<div class="alert alert-{{ type }}">
{{ message|trans({}, 'messages') }}
</div>
{% endfor %}
{% endfor %}
{% endif %}
If you dont want to use the flashBag, you can use the same logic to throw a customized exception with a specific message, catch it with a listener and display the message you want.
Ok I've had the same problem as you did. There is no easy way for this one you'd have to overwrite the default SecurityListener of Symfony Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener with something in those lines:
use Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener;
class MySecurityListener extends SecurityListener
{
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
if (!$configuration = $request->attributes->get('_acme_security')) {
return;
}
// trick to simulate one security configuration (all in one class/method).
$request->attributes->set('_security', new SecurityConfiguration($configuration));
if (!$this->language->evaluate($configuration->getExpression(), $this->getVariables($request))) {
throw new AccessDeniedException(sprintf($configuration->getMessage());
}
parent::onKernelController($event);
}
}
On top of that you'll need to extend Sensio\Bundle\FrameworkExtraBundle\Configuration\Security like so:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security as SensioSecurity;
/**
* #Annotation
*/
class Security extends SensioSecurity
{
protected $message;
public function getAliasName()
{
return 'acme_security';
}
public function getMessage()
{
return $this->message;
}
}
This above will allow you to add a message property to your controller annotation and that will be used if there is AccessDenied exception.
And here how to configure your security listener in Yaml:
acme.security.listener:
class: AppBundle\EventListener\SecurityListener
parent: sensio_framework_extra.security.listener
tags:
- { name: kernel.event_subscriber }
I am trying to just execute my Document's __constructor on pre_deserialization via jmsserializer but I don't have a clue why it is not working.
I am loading the serializer metadata from a yaml file looking like this:
AppBundle\Document\Campaign:
exclusion_policy: ALL
xml_root_name: campaign
properties:
id:
type: string
expose: true
slug:
type: string
expose: true
name:
type: string
expose: true
callback_methods:
pre_deserialize: [__construct]
When I try to deserialize executing:
$object = $serializer->deserialize($jsonString, 'AppBundle\\Document\\Campaign', 'json');
I am unable to reach the contructor function, however If I change the event to any of the others available (pre_serialize, post_serialize and post_deserialize) I do.
I think there are missing code about the handling of this specific event but trying to copy the same code affecting the other events it still not working.
It looks like it is never registered in the event dispatcher or something similar.
My environment is:
symfony 2.6.3
jms/serializer 0.16.0
jms/serializer-bundle 0.13.0
Thanks.
I can verify this appears to be a bug in JMS Serializer. For some reason, the service container is not reading the pre_deserialize events and registering it with JMS.
You can, however, work around this using an event subscriber.
First define the Subscriber class, similar to your listener:
<?php
namespace Acme\AcmeBundle\Listener;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
class SerializationSubscriber implements EventSubscriberInterface
{
/**
* #inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.pre_deserialize', 'method' => 'onPreDeserialize'),
);
}
public function onPreDeserialize(PreDeserializeEvent $event)
{
echo "we're about to de-cerealizing";
}
}
Then register the Subscriber in your bundle's services configuration:
parameters:
acme.serializer_subscriber.class: Acme\AcmeBundle\Listener\SerializationSubscriber
services:
acme.serializer.subscriber:
class: %acme.serializer_subscriber.class%
tags:
- { name: jms_serializer.event_subscriber }
Rebuild your cache, and you should be good!
Official Documentation: http://jmsyst.com/libs/serializer/master/event_system
I'm trying to do an internationalized website, with an URL prefix for each language I translated (eg. /fr/my/page or /it/my/page).
I tried JMSI18nRoutingBundle and it works pretty good with almost no additional configuration. But I really want to determine automatically the user preferred language.
The user's favorite languages are transmitted into the Accept-Language HTTP header, and I want to choose the first language I have a translation for.
Here is my JMSI18nRouting config:
jms_i18n_routing:
default_locale: en
locales: [fr, en]
strategy: prefix_except_default
I want this type of behaviour:
http://mywebsite.com/my/page do an automatic language detection then a redirection to /xx/... (where xx is the user favorite language) because language is not specified in URL — Presently the default language is EN.
http://mywebsite.com/XX/my/page shows the page in XX language — Presently, works fine.
Any idea to do this ? Is the config OK ?
Oh, and, if anyone has a solution to do the same thing in pure Symfony (without JMSI18nRoutingBundle), my ears are widely open.
EDIT / Found a way to have intelligent redirections with JMSI18nRoutingBundle to respect user's favorite language or let user force the display of a language. See my answer.
Finally, I answer my question.
I developed a small "patch" that uses JMSI18nRoutingBundle and detects the user's preferred language, and also let the user force a language.
Create listener YourBundle/EventListener/LocaleListener.php
This listener will change the URL if the user's preferred locale is different to the locale defined by Symfony or JMSI18nRoutingBundle. In this way, you have two URL for two different contents in two different languages : it's SEO friendly.
You can also create a language selector composed of links hrefing to ?setlang=xx where xx is the language the user wants to display. The listener will detect the setlang query and will force the display of the xx lang, including in the next requests.
Note the $this->translatable = [... array. It let you define what parts of your site are translated/translatable. The granularity can be defined from the vendor to the action method.
You can also create a config node to define your translatable vendors/bundles/controllers, I don't made this because of performance considerations.
<?php
namespace YourVendor\YourBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
private $acceptedLocales;
private $translatable;
public function __construct($router, $defaultLocale, $acceptedLocales)
{
$this->router = $router;
$this->defaultLocale = $defaultLocale;
$this->acceptedLocales = $acceptedLocales;
$this->translatable = [
'Vendor1',
'Vendor2\Bundle1',
'Vendor2\Bundle2\Controller1',
'Vendor2\Bundle2\Controller2::myPageAction',
];
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$route = $request->get('_route');
if(!empty($newLocale = $request->query->get('setlang'))) {
if(in_array($newLocale, $this->acceptedLocales)) {
$cookie = new Cookie('force_lang', $newLocale, time() + 3600 * 24 * 7);
$url = $this->router->generate($route, ['_locale' => $newLocale] + $request->attributes->get('_route_params'));
$response = new RedirectResponse($url);
$response->headers->setCookie($cookie);
$event->setResponse($response);
}
} else if($this->translatable($request->attributes->get('_controller'))) {
$preferred = empty($force = $request->cookies->get('force_lang')) ? $request->getPreferredLanguage($this->acceptedLocales) : $force;
if($preferred && $request->attributes->get('_locale') != $preferred) {
$url = $this->router->generate($route, ['_locale' => $preferred] + $request->attributes->get('_route_params'));
$event->setResponse(new RedirectResponse($url));
}
}
}
private function translatable($str)
{
foreach($this->translatable as $t) {
if(strpos($str, $t) !== false) return true;
}
return false;
}
public static function getSubscribedEvents()
{
return [ KernelEvents::REQUEST => [['onKernelRequest', 200]] ];
}
}
Bind your listener on the HTTP kernel.
Edit your services.yml file.
services:
app.event_listener.locale_listener:
class: YourVendor\YourBundle\EventListener\LocaleListener
arguments: ["#router", "%kernel.default_locale%", "%jms_i18n_routing.locales%"]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Configuration of JMSI18nRoutingBundle
You have nothing to change.
Example:
# JMS i18n Routing Configuration
jms_i18n_routing:
default_locale: "%locale%"
locales: [fr, en]
strategy: prefix_except_default
Here's a method to do it using straight Symfony. It might feel a tad hacky because it requires specifying 2 routes per each action, so if someone can think of a better way I'm all ears.
First, I would define some sort of config parameter for all of the acceptable locales, and list the first one as the default
parameters.yml.dist:
parameters:
accepted_locales: [en, es, fr]
Then make sure your Controller routes match for when _locale is both set and not set. Use the same route name for both, except suffix the one without a _locale with a delimiter like |:
/**
* #Route("/{_locale}/test/{var}", name="test")
* #Route( "/test/{var}", name="test|")
*/
public function testAction(Request $request, $var, $_locale = null)
{
// whatever your controller action does
}
Next define a service that will listen on the Controller event and pass your accepted locales to it:
<service id="kernel.listener.locale" class="My\Bundle\EventListener\LocaleListener">
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
<argument>%accepted_locales%</argument>
</service>
Now use the service to detect if _locale is set in your route, and if not, determine the locale based on the HTTP_ACCEPT_LANGUAGE header and redirect to the route that contains it. Here's an example listener that will do this (I added comments to explain what I was doing):
namespace NAB\UtilityBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class ControllerListener
{
private $acceptedLocales;
public function __construct(array $acceptedLocales)
{
$this->acceptedLocales = $acceptedLocales;
}
public function onKernelController(FilterControllerEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$request = $event->getRequest();
$params = $request->attributes->get('_route_params');
// return if _locale is already set on the route
if ($request->attributes->get('_locale')) {
return;
}
// if the user has accepted languages set, set the locale on the first match found
$languages = $request->server->get('HTTP_ACCEPT_LANGUAGE');
if (!empty($languages))
{
foreach (explode(',', $languages) as $language)
{
$splits = array();
$pattern = '/^(?P<primarytag>[a-zA-Z]{2,8})(?:-(?P<subtag>[a-zA-Z]{2,8}))?(?:(?:;q=)(?P<quantifier>\d\.\d))?$/';
// if the user's locale matches the accepted locales, set _locale in the route params
if (preg_match($pattern, $language, $splits) && in_array($splits['primarytag'], $this->acceptedLocales))
{
$params['_locale'] = $splits['primarytag'];
// stop checking once the first match is found
break;
}
}
}
// if no locale was found, default to the first accepted locale
if (!$params['_locale']) {
$params['_locale'] = $this->acceptedLocales[0];
}
// drop the '|' to get the appropriate route name
list($localeRoute) = explode('|', $request->attributes->get('_route'));
// attempt get the redirect URL but return if it could not be found
try {
$redirectUrl = $controller[0]->generateUrl($localeRoute, $params);
}
catch (\Exception $e) {
return;
}
// set the controller response to redirect to the route we just created
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
For further explanation on setting up a before filter on a Controller, check out the Symfony documentation here. If you use something like this, be very careful that every route name is defined properly.
Another more usable solution
Go to the vendor of I18nRoutingBundle and edit the listener
/www/vendor/jms/i18n-routing-bundle/JMS/I18nRoutingBundle/EventListener
Replace
$locale = $this->localeResolver->resolveLocale($request, $this->locales) ?: $this->defaultLocale;
by
$locale = $this->localeResolver->resolveLocale($request, $this->locales) ?: $request->getPreferredLanguage($this->locales);
(It is cleaner to overide the listener than to directly edit the vendors)
I prepare external bundle and I would like to add some routes during compilation passes.
Routes will be created on the main app/config/config.yml settings.
I was trying to get router from ContainerBuilder in my CustomCompilerPass via:
$definition = $container->getDefinition('router');
, but I got The service definition "router" does not exist.
Is it possible to add custom routes during compilation passes?
There's no way to add routes at compiler passes.
In order to dynamicly load routes (aware of container parameters) I'd use a custom route loader as given in my previous example
class MyLoader extends Loader
{
protected $params;
public function __construct($params)
{
$this->params = $params;
}
public function supports($resource, $type = null)
{
return $type === 'custom' && $this->params == 'YourLogic';
}
public function load($resource, $type = null)
{
// This method will only be called if it suits the parameters
$routes = new RouteCollection;
$resource = '#AcmeFooBundle/Resources/config/dynamic_routing.yml';
$type = 'yaml';
$routes->addCollection($this->import($resource, $type));
return $routes;
}
}
routing.yml
_custom_routes:
resource: .
type: custom
router is an alias, not a service. To get that from a ContainerBuilder, use ContainerBuilder::getAlias. To get the service ID, you need to cast that object to a string: (string) $container->getAlias('router'). Now, you can use that ID to get the service: $container->getDefinition($container->getAlias('router')). And then you get the Service which you can modify to add routes.
BTW, I'm not sure if this is really the thing you want. What about using the CmfRoutingBundle. Then, you use the Chain Router, so you can use both the Symfony2 router and the DynamicRouter. The DynamicRouter can be used with a custom route provider, in which you return the routes you want (you can get them from every resource you want).