Symfony override _locale parameter - php

I have created a custom LocaleListener:
class LocaleListener implements EventSubscriberInterface
{
public function onKernelRequest(GetResponseEvent $event)
{
if ($request->getSession()->get("language")) {
$lang = $request->getSession()->get("language");
} else {
$lang = $request->getPreferredLanguage(array("de", "en"));
}
$request->getSession()->set('_locale', $lang);
$request->setLocale($request->getSession()->get('_locale', $lang));
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
I get the language code from session (if exists) or from browser. Now I would like to correct / manipulate the route. I am using _locale in my router to set the language code (en or de). Now I would like override the user's choice.
For example, the language code in the LocaleListener is de. The user does now request the url which _locale = en (example: /en instead of automatically /de).
How can I force to set the users _locale to my result form the LocaleListener outside of every controller, with a listener or an other solution?

In this case you need to add RedirectResponse
$event->setResponse(new RedirectResponse($request->getBaseUrl().'/'.$locale.'/'.($params ? '?'.http_build_query($params) : '')));
or use JMSI18nRoutingBundle https://github.com/schmittjoh/JMSI18nRoutingBundle

Related

Silverstripe: Not able to access Page fields in controller with custom route

I'm using SilverStripe 3.3.1 and have a custom route set up to handle urls with many parameters. That works.
However, the routing rule causes Page fields and functions to be inaccessible in the Page_Controller and templates. Any ideas how to fix this?
//MyPage class
class MyPage extends Page {
//Not accessible if route to controller specified in config.yml
private static $db = array(
'MyPageVar' => 'Int',
);
//Not accessible if route to controller specified in config.yml
public function getMySpecialVar() {
return $this->MyPageVar;
}
}
//MyPage_Controller class
class MyPage_Controller extends Page_Controller {
private static $allowed_actions = array(
'index',
'detailsearch',
);
private static $url_handlers = array (
'detailsearch/$Key1/$Value1/$Key2/$Value2/$Key3/$Value3/$Key4/$Value4/$Key5/$Value5' => 'detailsearch',
);
/**
* UseMyPageVar()
*
* #return Boolean
*/
public function UseMyPageVar() {
//Empty if route to controller specified in config.yml
Debug::show($this->MyPageVar);
Debug::show($this->Title);
Debug::show($this->Content);
//Error if route to controller specified in config.yml
Debug::show($this->getMySpecialVar());
return true;
}
}
MyPage.ss
<!-- This work as expected if no route is specified. -->
<!-- But all vars are empty if route is specified in config.yml -->
<p>MyVar: $MyPageVar</p>
<p>Title: $Title</p>
<p>Content: $Content</p>
Routing rule in config.yml
Director:
rules:
'mypage': 'MyPage_Controller'
This question is also posted on the Silverstripe forum:
http://www.silverstripe.org/community/forums/general-questions/editpost/413506
It's not pretty, but for now I've solved the problem by using a private var in the Controller class to hold a reference to the page.
//MyPage_Controller class
class MyPage_Controller extends Page_Controller {
private $_page; //reference to page that's lost with custom routing
//ContentController uses route, which has been changed to
// 'MyPage_Controller' by routing rule, to initialize
// page reference. Can't find anything so reference
// not set. (set to -1)
public function init() {
parent::init();
//Initialize using default route overwritten in routing rule
// This will break if URL segment changed in CMS
$route = array_search($this->URLSegment,
Config::inst()->get('Director', 'rules'));
$link = str_replace($this->URLSegment, $route, $this->Link());
$this->_page = $this->Page($link);
}
//Use private var to access page fields
public function MyPageVar() {
Debug::show($this->_page->MyPageVar);
}
//expose $Content to templates
public function Content() {
return $this->_page->Content;
}
//Can't use Title() so expose Page Title as $PageTitle
public function PageTitle() {
return $this->_page->Title;
}
}
Three things spring to mind when I look at your code:
That "mypage" in config.yml should be the name of a public method on MyPage_Controller. As it is, SilverStripe cannot find a matching method called mypage and will default to calling index() instead.
Routes should really go in a separate routes.yml file so you can "namespace" it to be invoked before or after SilverStripe's own core routes. If you don't do this, then it may result in the weird behaviour you're experiencing.
Did you know that you can debug your routes using the ?debug_request=1 URL param? See: https://docs.silverstripe.org/en/3.3/developer_guides/debugging/url_variable_tools#general-testing

beforeFilter function not redirecting in Symfony2

I have implemented following code to run a code on before any action of any controller. However, the beforeFilter() function not redirecting to the route I have specified. Instead it takes the user to the location where the user clicked.
//My Listener
namespace Edu\AccountBundle\EventListener;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class BeforeControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
//not a controller do nothing
return;
}
$controllerObject = $controller[0];
if (is_object($controllerObject) && method_exists($controllerObject, "beforeFilter"))
//Set a predefined function to execute Before any controller Executes its any method
{
$controllerObject->beforeFilter();
}
}
}
//I have registered it already
//My Controller
class LedgerController extends Controller
{
public function beforeFilter()
{
$commonFunction = new CommonFunctions();
$dm = $this->getDocumentManager();
if ($commonFunction->checkFinancialYear($dm) == 0 ) {
$this->get('session')->getFlashBag()->add('error', 'Sorry');
return $this->redirect($this->generateUrl('financialyear'));//Here it is not redirecting
}
}
}
public function indexAction() {}
Please help, What is missing in it.
Thanks Advance
I would suggest you follow the Symfony suggestions for setting up before and after filters, where you perform your functionality within the filter itself, rather than trying to create a beforeFilter() function in your controller that is executed. It will allow you to achieve what you want - the function being called before every controller action - as well as not having to muddy up your controller(s) with additional code. In your case, you would also want to inject the Symfony session to the filter:
# app/config/services.yml
services:
app.before_controller_listener:
class: AppBundle\EventListener\BeforeControllerListener
arguments: ['#session', '#router', '#doctrine_mongodb.odm.document_manager']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Then you'll create your before listener, which will need the Symony session and routing services, as well as the MongoDB document manager (making that assumption based on your profile).
// src/AppBundle/EventListener/BeforeControllerListener.php
namespace AppBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use AppBundle\Controller\LedgerController;
use AppBundle\Path\To\Your\CommonFunctions;
class BeforeControllerListener
{
private $session;
private $router;
private $documentManager;
private $commonFunctions;
public function __construct(Session $session, Router $router, DocumentManager $dm)
{
$this->session = $session;
$this->router = $router;
$this->dm = $dm;
$this->commonFunctions = new CommonFunctions();
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof LedgerController) {
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'Sorry');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
If you are in fact using the Symfony CMF then the Router might actually be ChainRouter and your use statement for the router would change to use Symfony\Cmf\Component\Routing\ChainRouter;
There are a few additional things here you might want to reconsider - for instance, if the CommonFunctions class needs DocumentManager, you might just want to make your CommonFunctions class a service that injects the DocumentManager automatically. Then in this service you would only have to inject your common functions service instead of the document manager.
Either way what is happening here is that we are checking that we are in the LedgerController, then checking whether or not we want to redirect, and if so we overwrite the entire Controller via a callback. This sets the redirect response to your route and performs the redirect.
If you want this check on every single controller you could simply eliminate the check for LedgerController.
.
$this->redirect() controller function simply creates an instance of RedirectResponse. As with any other response, it needs to be either returned from a controller, or set on an event. Your method is not a controller, therefore you have to set the response on the event.
However, you cannot really set a response on the FilterControllerEvent as it is meant to either update the controller, or change it completely (setController). You can do it with other events, like the kernel.request. However, you won't have access to the controller there.
You might try set a callback with setController which would call your beforeFilter(). However, you wouldn't have access to controller arguments, so you won't really be able to call the original controller if beforeFilter didn't return a response.
Finally you might try to throw an exception and handle it with an exception listener.
I don't see why making things this complex if you can simply call your method in the controller:
public function myAction()
{
if ($response = $this->beforeFilter()) {
return $response;
}
// ....
}
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
$response = new Response();
// Matched route
$_route = $request->attributes->get('_route');
// Matched controller
$_controller = $request->attributes->get('_controller');
$params = array(); //Your params
$route = $event->getRequest()->get('_route');
$redirectUrl = $url = $this->container->get('router')->generate($route,$params);
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
Cheers !!

Default value for route params in generated url

I have a site which is localized into several languages. Every public route is prefixed with the locale key (e.g. /{locale}/foo/bar), which gets caught and applied by middleware.
When generating URLs to point to other pages, I end up needing to feed the current locale into every url, like so:
Foo Bar
Otherwise the output url will contain %7Blocale%7D, which breaks it. This strikes me as needlessly verbose. Is there not a way to specify a default value for a given named parameter, such that if no value is explicitly provided for 'locale' it can be defaulted to whatever the current locale is?
I've inspected the UrlGenerator class, but I don't see anything to that effect.
The Route class has a defaults property, but that only appears to be used as part of binding the route to the current request.
Ultimately, not a huge issue, just wondering if anyone has any ideas for ways to save a bit of sanity.
You can use URL defaults as well at a middleware:
use Illuminate\Support\Facades\URL;
URL::defaults(
[
'locale' => $locale
]
);
When you define your routes, use optional variables with defaults:
Routes:
Route::get('{locale?}/foo/bar', 'Controller#fooBar');
Controller:
public function __construct()
{
$this->locale = session()->has('locale') ? session('locale') : 'en';
}
public function fooBar($locale = null)
{
$locale = $locale ?: $this->locale;
}
OR:
public function fooBar($locale = 'en')
{
$locale = $locale ?: $this->locale;
}
Wherever you call your route:
Foo Bar
You could optionally put the constructor in a BaseController class that all your other controllers extend.
There may be better ways to do this, but this would keep you from having to include the locale wherever you call a route.
There isn't any built in means of doing this, but I managed to achieve the desired result by extending the UrlGenerator
<?php
namespace App\Services;
use Illuminate\Routing\UrlGenerator as BaseGenerator;
use Illuminate\Support\Arr;
class UrlGenerator extends BaseGenerator
{
protected $default_parameters = [];
public function setDefaultParameter($key, $value){
$this->default_parameters[$key] = $value;
}
public function removeDefaultParameter($key){
unset($this->default_parameters[$key]);
}
public function getDefaultParameter($key){
return isset($this->default_parameters[$key]) ? $this->default_parameters[$key] : null;
}
protected function replaceRouteParameters($path, array &$parameters)
{
if (count($parameters) || count($this->default_parameters)) {
$path = preg_replace_sub(
'/\{.*?\}/', $parameters, $this->replaceNamedParameters($path, $parameters)
);
}
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
}
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
return isset($parameters[$m[1]]) ? Arr::pull($parameters, $m[1]) : ($this->getDefaultParameter($m[1]) ?: $m[0]);
}, $path);
}
}
Then rebinding our subclass into the service container
class RouteServiceProvider extends ServiceProvider
{
public function register(){
parent::register();
//bind our own UrlGenerator
$this->app['url'] = $this->app->share(function ($app) {
$routes = $app['router']->getRoutes();
$url = new UrlGenerator(
$routes, $app->rebinding(
'request', function ($app, $request) {
$app['url']->setRequest($request);
}
)
);
$url->setSessionResolver(function () {
return $this->app['session'];
});
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
}
//...
}
Then all I needed to do was inject the default locale into the UrlGenerator from the Locale middleware
public function handle($request, Closure $next, $locale = null) {
//...
$this->app['url']->setDefaultParameter('locale', $locale);
return $next($request);
}
Now route('foo.bar') will automatically bind the current locale to the route, unless another is explicitly provided.

Symfony session cannot be retrieve in twig

I'm trying to make a two language app with symfony2.
I'm using Doctrine behavior and A2lixtranslationformbundle.
I have a listener which listen to change the locale:
namespace George\CoreBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
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;
}
// try to see if the locale has been set as a _locale routing parameter
//if ($locale = $request->attributes->get('_locale')) {
if ($locale = $request->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('onKernelRequest', 17)),
);
}
}
In the twig template i want to retrieve the translation trough the session param to see what locale we have:
{{ entity.translate(app.session.get('_locale')).title }}
But the app.session.get('_locale') does not return nothing. What is the problem why the session in the twig do not get this property i have test it in the listener everything looks fine.
In order to retrive the locale in twig you can use the following snippet
{{ app.request.locale }}
for your case will be
{{ entity.translate(app.request.locale).title }}

Edit Symfony behavior with AJAX actions

Assuming I have an application using a lot of AJAX requests.
Is there a way to edit Symfony behavior and autommatically call indexAjaxAction instead of indexAction when my request is AJAX made ?
I already know that I can test if a request is Ajax with the Request::isXmlHttpRequest() method but I want it to be autommatic (i.e without testing in each controllerAction).
Does a service/bundle already makes it ?
Example :
<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
{
public function indexAction($vars)
{
$request = $this->getRequest();
if($request->isXmlHttpRequest()) {
return $this->indexAjaxAction($vars);
}
// Do Stuff
}
public function indexAjaxAction($vars){ /* Do AJAX stuff */ }
}
becomes
<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
{
public function indexAction($vars) { }
public function indexAjaxAction($vars) { }
// Other functions
}
One way would be to use a slightly modified controller resolver that would be used instead of the current controller resolver in the regular KttpKernel::handleRaw process.
Please note that I may be wrong in my thinking here and it is untested.
The controller resolver class has the id controller_resolver.class which you could overwrite with your custom one in your config using
In your app/config/config.yml...
.. config stuff ..
parameters:
controller_resolver.class: Acme\SomeBundle\Controller\ControllerResolver
And then in your new ControllerResolver...
namespace Acme\SomeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver
as BaseControllerResolver;
class ControllerResolver extends BaseControllerResolver
{
/**
* {#inheritdoc
*/
public function getArguments(Request $request, $controller)
{
if (is_array($controller) && $request->isXmlHttpRequest()) {
$action = preg_replace(
'/^(.*?)Action$/',
'$1AjaxAction',
$controller[1]
);
try {
$r = new \ReflectionMethod($controller[0], $action);
return $this->doGetArguments(
$request,
$controller,
$r->getParameters()
);
} catch( \Exception $e) {
// Do nothing
}
}
return parent::getArguments($request, $controller);
}
}
This class just extends the current controller resolver and will attempt to use the youractionAjaxAction if it exists in the controller and then falls back to the regular resolver if it gets an error (method not found);
Alternatively you could just use...
if (is_array($controller) && $request->isXmlHttpRequest()) {
$controller[1] = preg_replace(
'/^(?P<action>.*?)Action$/',
'$1AjaxAction',
$controller[1]
);
}
return parent::getArguments($request, $controller);
.. which would just update the called action and then send it through to the regular resolver with no fall back, meaning that every action that could be called using an XmlHttpRequest would require a corresponding AjaxAction.
You may want to look into FOSRestBundle for Symfony, it can be very useful if you have 1 action that can either return json data or rendered html template depending on the request method

Categories