I'd like to know if Payum (or PayumBundle) dispatch an event when the payment is captured?
The aim is to send a confirmation email to the customer.
I found nothing is the doc, and I found only three events in the code source :
payum.gateway.pre_execute
payum.gateway.execute
payum.gateway.post_execute
But I guess it has no connection with the payment itself. Btw, it seems never dispatched with a Stripe payment.
Thanks.
Payum does not dispatch any events by default. It is designed to be extended by an extension. For example if you want to do some stuff on an instance push notification (Paypal IPN) you can add an extension like one described here: http://payum.org/doc/1.0/Core/instant-payment-notification
There is also an extension which dispatch events. You have to manually create it and add to payum.
I encounter the same issue : dispatching a custom event on some payment actions. In my case I want to deal with Authorize and Capture actions.
I noticed that Payum requests may be processed many times for 1 payment action, that's why I need to mark my payment as processed in order to dispatch only once my custom event (purpose of Payment::$customPaymentEventDispatched property).
Here is my Payment entity :
<?php
namespace MyApp\PaymentBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Payment as BasePayment;
class Payment extends BasePayment
{
// ...
// Not persisted
protected $payable;
protected $customPaymentEventDispatched = false;
// ...
public function setPayable($payable)
{
$this->payable = $payable;
return $this;
}
public function getPayable()
{
return $this->payable;
}
public function markAsCustomPaymentEventDispatched()
{
$this->customPaymentEventDispatched = true;
return $this;
}
public function isCustomPaymentEventDispatched()
{
return $this->customPaymentEventDispatched;
}
}
Then register the Payum eventDispatcherExtension and my custom listener
services:
payum.extension.event_dispatcher:
class: Payum\Core\Bridge\Symfony\Extension\EventDispatcherExtension
arguments: ["#event_dispatcher"]
tags:
- { name: payum.extension, all: true, prepend: false }
my_app_payment.payum.listener.payment_listener:
class: MyApp\PaymentBundle\EventListener\PaymentListener
arguments:
- "#monolog.logger.payment"
tags:
- { name: kernel.event_listener, event: payum.gateway.post_execute }
And the code of the listener :
<?php
namespace MyApp\PaymentBundle\EventListener;
use Payum\Core\Bridge\Symfony\Event\ExecuteEvent;
use Payum\Core\Request\Authorize;
use Payum\Core\Request\BaseGetStatus;
use Payum\Core\Request\Capture;
use Payum\Core\Model\PaymentInterface;
use Payum\Core\Request\GetHumanStatus;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use MyApp\PaymentBundle\Entity\Payment;
use MyApp\PaymentBundle\Event\PayablePaymentStatusChangeEvent;
use MyApp\PaymentBundle\PaymentEvents;
class PaymentListener
{
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onPayumGatewayPostexecute(ExecuteEvent $event, $eventName, EventDispatcherInterface $eventDispatcher)
{
$request = $event->getContext()->getRequest();
// Exclude Payum GetStatus request
// => protect from infinite loop as following code execute one GetStatus request
if ($request instanceof BaseGetStatus) {
return;
}
// Request types requiring dispatching an event
if (!($request instanceof Authorize || $request instanceof Capture)) {
return;
}
$payment = $request->getFirstModel();
if (!$payment instanceof PaymentInterface) {
return;
}
$event->getContext()->getGateway()->execute($status = new GetHumanStatus($payment));
if (!$payment->isCustomPaymentEventDispatched()) {
if ($request instanceof Capture && $status->isCaptured())
{
$payable = $payment->getPayable();
$this->logger->info('Dispatching PAYMENT_SUCCESS for '.$payable->__toString());
$tmEvent = new PayablePaymentStatusChangeEvent($payment->getPayable());
$eventDispatcher->dispatch(PaymentEvents::PAYMENT_PENDING, $tmEvent);
$payment->markAsCustomPaymentEventDispatched();
}
elseif ($request instanceof Authorize && $status->isAuthorized())
{
$payable = $payment->getPayable();
$this->logger->info('Dispatching PAYMENT_PENDING for '.$payable->__toString());
$tmEvent = new PayablePaymentStatusChangeEvent($payment->getPayable());
$eventDispatcher->dispatch(PaymentEvents::PAYMENT_PENDING, $tmEvent);
$payment->markAsCustomPaymentEventDispatched();
}
}
}
}
I'm not fully satisfied of this implementation but I did not find out a better way to achieve that. I'll appreciate any feedback about this !
That's how you can check whether payment is captured or not:
use Payum\Core\Action\CapturePaymentAction;
use Payum\Core\Bridge\Symfony\Event\ExecuteEvent;
use Payum\Core\Request\Capture;
use Payum\Core\Request\GetHumanStatus;
class SuccessPaymentListener
{
public function onPostExecute(ExecuteEvent $event) {
$context = $event->getContext();
$action = $context->getAction();
$request = $context->getRequest();
if ($action instanceof CapturePaymentAction && $request instanceof Capture) {
$status = new GetHumanStatus($request->getToken());
$gateway = $context->getGateway();
$gateway->execute($status);
if ($status->isCaptured()) {
$payment = $status->getFirstModel();
//here is your stuff
}
}
}
}
services.yml:
app.payum.extension.event_dispatcher:
class: Payum\Core\Bridge\Symfony\Extension\EventDispatcherExtension
arguments: ["#event_dispatcher"]
tags:
- { name: payum.extension, all: true, prepend: false }
app.success_payment_listener:
class: AppBundle\Listener\SuccessPaymentListener
tags:
- { name: kernel.event_listener, event: payum.gateway.post_execute, method: onPostExecute }
I have a problem with translation. I use
symfony 2.7
sonata admin-bundle 2.3
I have create the interactive login listener, when the user log in the application I get the user locale and set the session _locale, but this is ignore in sonata.
Listener is
class UserLocaleListener {
/**
* #var Session
*/
private $container;
public function __construct(Session $session)
{
$this->session = $session;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->session->set('_locale', $user->getLocale());
//$request->setLocale($user->getLocale());
var_dump($request->getSession()->get('_locale'));
}
}
}
in service.yml add
app.user_locale_listener:
class: xxxxxx\xxxxxxxx\EventListener\UserLocaleListener
arguments: ["#session"]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
Where is my mistake ?
the locale is set on the request, and will not "stick" , so each request it will be the default again, unless you do something like this:
http://symfony.com/doc/current/cookbook/session/locale_sticky_session.html
Wich will on each request, take the locale from the session, and set it on the current request.
(make sure that LocaleListener has a lower priority then your UserLocaleListener, so that it runs after it)
I want to store the last locale used by a user on every Request the user makes. I already created a Field in the Database and only need the best practice way without big modifications in every Controller.
Thank you all.
You could store your locale in session and use it with an event listener. And on user login set user locale in session.
-LocaleListener : Store the locale in session and user locale in session
-UserLocaleListener : On user login set user locale in session
<?php
namespace YourApp\YourBundle\EventListener;
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 onKernelRequest17(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
// if no explicit locale has been set on this request, use one from the session
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest17', 17)),
);
}
}
Second service:
<?php
namespace YourApp\YourBundle\EventListener;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class UserLocaleListener
{
private $session;
public function setSession(Session $session)
{
$this->session = $session;
}
/**
* kernel.request event. If a guest user doesn't have an opened session, locale is equal to
* "undefined" as configured by default in parameters.ini. If so, set as a locale the user's
* preferred language.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function setLocaleForUnauthenticatedUser(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ('undefined' == $request->getLocale()) {
if ($locale = $request->getSession()->get('_locale')) {
$request->setLocale($locale);
} else {
$request->setLocale($request->getPreferredLanguage());
}
}
}
/**
* security.interactive_login event. If a user chose a language in preferences, it would be set,
* if not, a locale that was set by setLocaleForUnauthenticatedUser remains.
*
* #param \Symfony\Component\Security\Http\Event\InteractiveLoginEvent $event
*/
public function setLocaleForAuthenticatedUser(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if ($lang = $user->getLocale()) {
$event->getRequest()->setLocale($lang);
$this->session->set('_locale', $lang);
}
}
}
services.yml
services:
yourapp_your.locale_listener:
class: YourApp\YourBundle\EventListener\LocaleListener
arguments: ["%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
yourapp_your.locale.interactive_login_listener:
class: YourApp\YourBundle\EventListener\UserLocaleListener
calls:
- [ setSession, [#session] ]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: setLocaleForAuthenticatedUser }
This is using
https://github.com/symfony/symfony/blob/master/UPGRADE-2.1.md#simulate-old-behavior
Translations in Symfony 2.3 locale in request
Symfony2 locale detection: not considering _locale in session
symfony 2 set locale based on user preferences stored in DB
That is my implementation to save the locale on each request in the database.
It is necessary for me because i need the locale persisted for the newsletter.
LocaleListener:
namespace MyApp\MyBundle\EventListener;
use \Symfony\Component\HttpKernel\Event\GetResponseEvent;
use \Symfony\Component\HttpKernel\KernelEvents;
use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
use \FOS\UserBundle\Model\UserManagerInterface;
use \Symfony\Component\Security\Core\SecurityContext;
use \MyApp\MyBundle\Entity\User;
class LocaleListener implements EventSubscriberInterface
{
private $userManager;
private $securityContext;
private $defaultLocale;
public function __construct(UserManagerInterface $userManager, SecurityContext $securityContext, $defaultLocale = 'en')
{
$this->userManager = $userManager;
$this->securityContext = $securityContext;
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest17(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
// try to see if the locale has been set as a _locale routing parameter
$locale = $request->attributes->get('_locale');
if ($locale !== null)
{
$request->getSession()->set('_locale', $locale);
}
else
{
// if no explicit locale has been set on this request, use one from the session
$locale = $request->getSession()->get('_locale', $this->defaultLocale);
$request->setLocale($locale);
}
// save last locale to the user if he is logged in
$user = $this->getUser();
if($user instanceof User)
{
$user->setDefaultLanguage($locale);
$this->userManager->updateUser($user);
}
}
private function getUser()
{
$token = $this->securityContext->getToken();
if($token !== null)
return $this->securityContext->getToken()->getUser();
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest17', 17)),
);
}
}
services.yml:
myApp_myBundle.locale_listener:
class: MyApp\MyBundle\EventListener\LocaleListener
arguments: [#fos_user.user_manager, #security.context, "%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
I'm working in a project using Symfony 2,
I'm using Assetic with rewrite and less filter, and it work fine,
Now I'm planing to let administrator (connected user) to controle some features in css like font and main color.
The problem that I'm facing is :
- how can I proceed to integrate these css changes from entity to the css management
I can't let assetic use routing rule to include custom css
Eaven if I success to get this work, every time I have a changes to the custom css I have to install assets to web folder and make the assetic:dump and clearing cache from a controller.
If you (or someone else) still need this:
I solved this by putting all generic CSS in a asset handled by Assetic like usual and putting the dynamic CSS generation in a Controller action and rendering the CSS with Twig.
As suggested by Steffen you should put the dynamic CSS in a Twig template.
But now you might suffer from that part of the css being a full request to a symfony application instead of a css (HTTP 302 and such) which increases server load.
Thats why I would advise you to do 3 things (you can skip step 2 if your css doesn't change without interaction, e.g. date based):
Implement a service which caches the current output to e.g. web/additional.css.
Write and register a RequestListener to update the css regularly
Extend all controller actions that could introduce changes to the css with the service call
Example (assumes you use Doctrine and have an entity with some color information):
Service
<?php
//Acme\DemoBundle\Service\CSSDeployer.php
namespace Acme\DemoBundle\Service;
use Doctrine\ORM\EntityManager;
class CSSDeployer
{
/**
* #var EntityManager
*/
protected $em;
/**
* Twig Templating Service
*/
protected $templating;
public function __construct(EntityManager $em, $templating)
{
$this->em = $em;
$this->templating = $templating;
}
public function deployStyle($filepath)
{
$entity = $this->em->getRepository('AcmeDemoBundle:Color')->findBy(/* your own logic here */);
if(!$entity) {
// your error handling
}
if(!file_exists($filepath)) {
// your error handling, be aware of the case where this service is run the first time though
}
$content = $this->templating->render('AcmeDemoBundle:CSS:additional.css.twig', array(
'data' => $entity
));
//Maybe you need to wrap below in a try-catch block
file_put_contents($filepath, $content);
}
}
Service Registration
#Acme\DemoBundle\Resources\config\services.yml
services:
#...
css_deployer:
class: Acme\DemoBundle\Service\CSSDeployer
arguments: [ #doctrine.orm.entity_manager, #templating ]
RequestListener
<?php
//Acme\DemoBundle\EventListener\RequestListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use \DateTime;
use Doctrine\ORM\EntityManager;
class RequestListener
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var EntityManager
*/
protected $em;
public function __construct(ContainerInterface $container, $em)
{
$this->container = $container;
$this->em = $em;
}
/**
* Checks filemtime (File modification time) of web/additional.css
* If it is not from today it will be redeployed.
*/
public function onKernelRequest(GetResponseEvent $event)
{
$kernel = $event->getKernel();
$container = $this->container;
$path = $container->get('kernel')->getRootDir().'/../web'.'/additional.css';
$time = 1300000000;
try {
$time = #filemtime($path);
} catch(ContextErrorException $ex) {
//Ignore
} catch(\Exception $ex) {
//will never get here
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))) {
throw $ex;
}
}
if($time === FALSE || $time == 1300000000) {
file_put_contents($path, "/*Leer*/");
$time = 1300000000;
}
$modified = new \DateTime();
$modified->setTimestamp($time);
$today = new \DateTime();
if($modified->format("Y-m-d")!= $today->format("Y-m-d")) {
//UPDATE CSS
try {
$container->get('css_deployer')->deployStyle($path);
} catch(\Exception $ex) {
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))){
throw $ex;
}
}
} else {
//DO NOTHING
}
}
}
RequestListener registration
#Acme\DemoBundle\Resources\config\services.yml
acme_style_update_listener.request:
class: Acme\DemoBundle\EventListener\RequestListener
arguments: [ #service_container, #doctrine.orm.entity_manager ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Controller actions
public function updateAction()
{
// Stuff
$path = '....';
$this->get('css_deployer')->deployStyle($path);
}
Hope this helps someone in the future.
I have a problem with routing and the internationalization of my site built with Symfony2.
If I define routes in the routing.yml file, like this:
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
It works fine with URLs like:
mysite.com/en/example
mysite.com/fr/example
But doesn't work with
mysite.com/example
Could it be that optional placeholders are permitted only at the end of an URL ?
If yes, what could be a possible solution for displaying an url like :
mysite.com/example
in a default language or redirecting the user to :
mysite.com/defaultlanguage/example
when he visits :
mysite.com/example. ?
I'm trying to figure it out but without success so far.
Thanks.
If someone is interested in, I succeeded to put a prefix on my routing.yml without using other bundles.
So now, thoses URLs work :
www.example.com/
www.example.com//home/
www.example.com/fr/home/
www.example.com/en/home/
Edit your app/config/routing.yml:
ex_example:
resource: "#ExExampleBundle/Resources/config/routing.yml"
prefix: /{_locale}
requirements:
_locale: |fr|en # put a pipe "|" first
Then, in you app/config/parameters.yml, you have to set up a locale
parameters:
locale: en
With this, people can access to your website without enter a specific locale.
You can define multiple patterns like this:
example_default:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
example:
pattern: /{_locale}/example
defaults: { _controller: ExampleBundle:Example:index}
requirements:
_locale: fr|en
You should be able to achieve the same sort of thing with annotations:
/**
* #Route("/example", defaults={"_locale"="fr"})
* #Route("/{_locale}/example", requirements={"_locale" = "fr|en"})
*/
Hope that helps!
This is what I use for automatic locale detection and redirection, it works well and doesn't require lengthy routing annotations:
routing.yml
The locale route handles the website's root and then every other controller action is prepended with the locale.
locale:
path: /
defaults: { _controller: AppCoreBundle:Core:locale }
main:
resource: "#AppCoreBundle/Controller"
prefix: /{_locale}
type: annotation
requirements:
_locale: en|fr
CoreController.php
This detects the user's language and redirects to the route of your choice. I use home as a default as that it the most common case.
public function localeAction($route = 'home', $parameters = array())
{
$this->getRequest()->setLocale($this->getRequest()->getPreferredLanguage(array('en', 'fr')));
return $this->redirect($this->generateUrl($route, $parameters));
}
Then, the route annotations can simply be:
/**
* #Route("/", name="home")
*/
public function indexAction(Request $request)
{
// Do stuff
}
Twig
The localeAction can be used to allow the user to change the locale without navigating away from the current page:
{{ targetLanguage }}
Clean & simple!
The Joseph Astrahan's solution of LocalRewriteListener works except for route with params because of $routePath == "/{_locale}".$path)
Ex : $routePath = "/{_locale}/my/route/{foo}" is different of $path = "/{_locale}/my/route/bar"
I had to use UrlMatcher (link to Symfony 2.7 api doc) for matching the actual route with the url.
I change the isLocaleSupported for using browser local code (ex : fr -> fr_FR). I use the browser locale as key and the route locale as value. I have an array like this array(['fr_FR'] => ['fr'], ['en_GB'] => 'en'...) (see the parameters file below for more information)
The changes :
Check if the local given in request is suported. If not, use the default locale.
Try to match the path with the app route collection. If not do nothing (the app throw a 404 if route doesn't exist). If yes, redirect with the right locale in route param.
Here is my code. Works for any route with or without param. This add the locale only when {_local} is set in the route.
Routing file (in my case, the one in app/config)
app:
resource: "#AppBundle/Resources/config/routing.yml"
prefix: /{_locale}/
requirements:
_locale: '%app.locales%'
defaults: { _locale: %locale%}
The parameter in app/config/parameters.yml file
locale: fr
app.locales: fr|gb|it|es
locale_supported:
fr_FR: fr
en_GB: gb
it_IT: it
es_ES: es
services.yml
app.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
LocaleRewriteListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var urlMatcher \Symfony\Component\Routing\Matcher\UrlMatcher;
*/
private $urlMatcher;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'fr', array $supportedLocales, $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
$context = new RequestContext("/");
$this->matcher = new UrlMatcher($this->routeCollection, $context);
}
public function isLocaleSupported($locale)
{
return array_key_exists($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivalent when exists.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
// Do nothing if the route requested has no locale param
$request = $event->getRequest();
$baseUrl = $request->getBaseUrl();
$path = $request->getPathInfo();
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
if ($this->isLocaleSupported($locale)) {
$locale = $this->supportedLocales[$locale];
} else if ($locale == ""){
$locale = $request->getDefaultLocale();
}
$pathLocale = "/".$locale.$path;
//We have to catch the ResourceNotFoundException
try {
//Try to match the path with the local prefix
$this->matcher->match($pathLocale);
$event->setResponse(new RedirectResponse($baseUrl.$pathLocale));
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
}
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Symfony3
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
requirements:
_locale: en|bg| # put a pipe "|" last
There is my Solution, it makes this process faster!
Controller:
/**
* #Route("/change/locale/{current}/{locale}/", name="locale_change")
*/
public function setLocaleAction($current, $locale)
{
$this->get('request')->setLocale($locale);
$referer = str_replace($current,$locale,$this->getRequest()->headers->get('referer'));
return $this->redirect($referer);
}
Twig:
<li {% if app.request.locale == language.locale %} class="selected" {% endif %}>
{{ language.locale }}
</li>
I have a full solution to this that I discovered after some research. My solution assumes that you want every route to have a locale in front of it, even login. This is modified to support Symfony 3, but I believe it will still work in 2.
This version also assumes you want to use the browsers locale as the default locale if they go to a route like /admin, but if they go to /en/admin it will know to use en locale. This is the case for example #2 below.
So for example:
1. User Navigates To -> "/" -> (redirects) -> "/en/"
2. User Navigates To -> "/admin" -> (redirects) -> "/en/admin"
3. User Navigates To -> "/en/admin" -> (no redirects) -> "/en/admin"
In all scenarios the locale will be set correctly how you want it for use throughout your program.
You can view the full solution below which includes how to make it work with login and security, otherwise the Short Version will probably work for you:
Full Version
Symfony 3 Redirect All Routes To Current Locale Version
Short Version
To make it so that case #2 in my examples is possible you need to do so using a httpKernal listner
LocaleRewriteListener.php
<?php
//src/AppBundle/EventListener/LocaleRewriteListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'en', array $supportedLocales = array('en'), $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
}
public function isLocaleSupported($locale)
{
return in_array($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivlent as long as the route will exists when we do so.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
$request = $event->getRequest();
$path = $request->getPathInfo();
$route_exists = false; //by default assume route does not exist.
foreach($this->routeCollection as $routeObject){
$routePath = $routeObject->getPath();
if($routePath == "/{_locale}".$path){
$route_exists = true;
break;
}
}
//If the route does indeed exist then lets redirect there.
if($route_exists == true){
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
//If no locale from browser or locale not in list of known locales supported then set to defaultLocale set in config.yml
if($locale=="" || $this->isLocaleSupported($locale)==false){
$locale = $request->getDefaultLocale();
}
$event->setResponse(new RedirectResponse("/".$locale.$path));
}
//Otherwise do nothing and continue on~
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
To understand how that is working look up the event subscriber interface on symfony documentation.
To activate the listner you need to set it up in your services.yml
services.yml
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
# parameter_name: value
services:
# service_name:
# class: AppBundle\Directory\ClassName
# arguments: ["#another_service_name", "plain_value", "%parameter_name%"]
appBundle.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
Finally this refers to variables that need to be defined in your config.yml
config.yml
# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
app.locales: en|es|zh
locale_supported: ['en','es','zh']
Finally, you need to make sure all your routes start with /{locale} for now on. A sample of this is below in my default controller.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/{_locale}", requirements={"_locale" = "%app.locales%"})
*/
class DefaultController extends Controller
{
/**
* #Route("/", name="home")
*/
public function indexAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
/**
* #Route("/admin", name="admin")
*/
public function adminAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
}
?>
Note the requirements requirements={"_locale" = "%app.locales%"}, this is referencing the config.yml file so you only have to define those requirements in one place for all routes.
Hope this helps someone :)
We created a custom RoutingLoader that adds a localized version to all routes. You inject an array of additional locales ['de', 'fr'] and the Loader adds a route for each additional locale. The main advantage is, that for your default locale, the routes stay the same and no redirect is needed. Another advantage is, that the additionalRoutes are injected, so they can be configured differently for multiple clients/environments, etc. And much less configuration.
partial_data GET ANY ANY /partial/{code}
partial_data.de GET ANY ANY /de/partial/{code}
partial_data.fr GET ANY ANY /fr/partial/{code}
Here is the loader:
<?php
namespace App\Routing;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class I18nRoutingLoader extends Loader
{
const NAME = 'i18n_annotation';
private $projectDir;
private $additionalLocales = [];
public function __construct(string $projectDir, array $additionalLocales)
{
$this->projectDir = $projectDir;
$this->additionalLocales = $additionalLocales;
}
public function load($resource, $type = null)
{
$collection = new RouteCollection();
// Import directly for Symfony < v4
// $originalCollection = $this->import($resource, 'annotation')
$originalCollection = $this->getOriginalRouteCollection($resource);
$collection->addCollection($originalCollection);
foreach ($this->additionalLocales as $locale) {
$this->addI18nRouteCollection($collection, $originalCollection, $locale);
}
return $collection;
}
public function supports($resource, $type = null)
{
return self::NAME === $type;
}
private function getOriginalRouteCollection(string $resource): RouteCollection
{
$resource = realpath(sprintf('%s/src/Controller/%s', $this->projectDir, $resource));
$type = 'annotation';
return $this->import($resource, $type);
}
private function addI18nRouteCollection(RouteCollection $collection, RouteCollection $definedRoutes, string $locale): void
{
foreach ($definedRoutes as $name => $route) {
$collection->add(
$this->getI18nRouteName($name, $locale),
$this->getI18nRoute($route, $name, $locale)
);
}
}
private function getI18nRoute(Route $route, string $name, string $locale): Route
{
$i18nRoute = clone $route;
return $i18nRoute
->setDefault('_locale', $locale)
->setDefault('_canonical_route', $name)
->setPath(sprintf('/%s%s', $locale, $i18nRoute->getPath()));
}
private function getI18nRouteName(string $name, string $locale): string
{
return sprintf('%s.%s', $name, $locale);
}
}
Service definition (SF4)
App\Routing\I18nRoutingLoader:
arguments:
$additionalLocales: "%additional_locales%"
tags: ['routing.loader']
Routing definition
frontend:
resource: ../../src/Controller/Frontend/
type: i18n_annotation #localized routes are added
api:
resource: ../../src/Controller/Api/
type: annotation #default loader, no routes are added
I use annotations, and i will do
/**
* #Route("/{_locale}/example", defaults={"_locale"=""})
* #Route("/example", defaults={"_locale"="en"}, , requirements = {"_locale" = "fr|en|uk"})
*/
But for yml way, try some equivalent...
Maybe I solved this in a reasonably simple way:
example:
path: '/{_locale}{_S}example'
defaults: { _controller: 'AppBundle:Example:index' , _locale="de" , _S: "/" }
requirements:
_S: "/?"
_locale: '|de|en|fr'
Curious about the judgement of the critics ...
Best wishes,
Greg
root:
pattern: /
defaults:
_controller: FrameworkBundle:Redirect:urlRedirect
path: /en
permanent: true
How to configure a redirect to another route without a custom controller
I think you could simply add a route like this:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index }
This way, the locale would be the last locale selected by the user, or the default locale if user locale has not been set. You might also add the "_locale" parameter to the "defaults" in your routing config if you want to set a specific locale for /example:
example:
pattern: /example
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }
I don't know if there's a better way to do this.