I'm trying to utilize Symfony 4's multilang (or multi-locale) routing pattern.
My application is truly international and supports over 25 different languages, though the translations come incrementally and many routes are not translated to some languages yet.
In such case I want them to fall back into english-default.
My config/packages/translation.yaml looks like this:
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
My routes are defined within routes.yaml file. For example:
about_index:
path:
en: /about-us
pl: /o-nas
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: About/index.html.twig
Now, whenever I open the site with either pl or en locale - everything works as expected, but when for example I set it to de, I get "Unable to generate a URL for the named route "about_index" as such route does not exist." error.
How do I force Symfony to fallback to en paths whenever the route in desired locale does not yet exist?
So, after quite a bit of investigation it appears there's no way to make it work like that with Symfony's default methods.
I went for the "workaround" approach and extended Symfony's Twig Bridge's Routing extension with my own Twig function, autopath():
namespace App\Twig;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
use Twig\TwigFunction;
// auto-wired services
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Extends Symfony's Twig Bridge's routing extension providing more flexible localization.
*
* #author Kyeno
*/
class KyAutoPathExtension extends RoutingExtension
{
private $router;
private $request;
public function __construct(UrlGeneratorInterface $router, RequestStack $requestStack)
{
$this->router = $router;
$this->request = $requestStack->getCurrentRequest();
parent::__construct($router);
}
/**
* {#inheritdoc}
*
* #return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('autopath', [$this, 'getPathAuto'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']])
];
}
/**
* #param string $name
* #param array $parameters
* #param bool $relative
*
* #return string
*/
public function getPathAuto($name, $parameters = [], $relative = false)
{
// obtain current and default locales from request object
$localeRequested = $this->request->getLocale();
$localeDefault = $this->request->getDefaultLocale();
// build localized route name
// NOTE: Symfony does NOT RECOMMEND this way in their docs, but it's the fastest that popped in my mind
foreach([sprintf('%s.%s', $name, $localeRequested), sprintf('%s.%s', $name, $localeDefault)] as $nameLocalized) {
// if such route exists, link to it and break the loop
if($this->router->getRouteCollection()->get($nameLocalized)) {
return $this->router->generate($nameLocalized, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
}
// when no matches found, attempt relying on Symfony Twig Bridge's original path() function
// (and likely fail with exception, unless they fix/allow it)
return parent::getPath($name, $parameters, $relative);
}
}
Works on SF 4.4, using annotations : you can declare double #Route annotations, one with localized routes, the other without localized. The non-localized route will be used if not matching the first annotation.
* #Route({
* "fr": "/bonjour",
* "de": "/guten-tag"
* }, name="hello_path")
* #Route("/hello", name="hello_path")
Related
Using latest Symfony and FOSUserbundle, after successfully registering a new user, the user is automatically logged in. I want to prevent this. My reason is that only a special user should be able to register new users.
I guess I have to override the registerAction in the RegisterController of the bundle, but I don't know how.
I tried: http://symfony.com/doc/current/bundles/FOSUserBundle/overriding_controllers.html, but it seems to be outdated, no user is created with this method.
Any hints are appreciated.
Edit:
I found out that I did not create the child bundle correctly. I also had to create my own EventListener. It works now when I overwrite the FOSUserEvents::REGISTRATION_SUCCESS event.
Strange thing is that when I use the FOSUserEvents::REGISTRATION_COMPLETEDevent, both events are dispatched, my bundle's and the FOSUserbundle's, so that the user is redirected to the correct site, but logged in as the new user.
Edit 2:
So this is in my listener:
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
FOSUserEvents::REGISTRATION_COMPLETED => 'onRegistrationCompleted',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$url = $this->router->generate('admin');
$event->setResponse(new RedirectResponse($url));
}
public function onRegistrationCompleted(FilterUserResponseEvent $event)
{
}
I set the redirection in the REGISTRATION_SUCCESSevent and the REGISTRATION_COMPLETEDis empty. With the debugger I can verify that my own listener's event is called, but the original event is also called.
Actually, there is no need to do any of these. The fos_user.listener.authentication service is removed from the container if use_authentication_listener is set to false.
See line 74-76 in FOS\UserBundle\DependencyInjection\FOSUserExtension.
This information is also included in document FOS UserBundle Configuration.
You can solve this problem with a Listener, In fos user bundle, it authenticates user with after registration.
file :friendsofsymfony/user-bundle/EventListener/AuthenticationListener.php
class : FOS\UserBundle\EventListener\AuthenticationListener
If you check this class you would see it tracks REGISTRATION_COMPLETED Event.
In Authenticatiton Listener It dispatches Event after triggering logInUser function. Therefore you have to logout user in your listener which subscribes `REGISTRATION COMPLETED.
you can check https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/controller_events.rst for writing your listener to logout user.
Note : It may not be a good way log-in log-out user in every registration process, but if you use fosuserbundle easiest way and minimum footprint would be this, if there is already a yml configuration doesn't exists, actually in code there is no direction of yml conf. So this approach would be min. footprint.
try {
$this->loginManager->logInUser($this->firewallName, $event->getUser(), $event->getResponse());
$eventDispatcher->dispatch(FOSUserEvents::SECURITY_IMPLICIT_LOGIN, new UserEvent($event->getUser(), $event->getRequest()));
} catch (AccountStatusException $ex) {
// We simply do not authenticate users which do not pass the user
// checker (not enabled, expired, etc.).
}
EDIT: This technique works on Symfony 3.3, I'm unaware if this works on lower versions.
The correct way of doing this is by creating a Compiler Pass.
You can also: Override the service by adding a new service using the same name: fos_user.listener.authentication on your app/config.yml file or on your bundle config file and adding your new class to it as I've done below and add this
Here is how to override the automatic logging when registering a new user using the compiler pass technique.
The Compiler Pass
namespace arpa3\UserBundle\DependencyInjection;
use arpa3\UserBundle\EventListener\AuthenticationListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface {
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('fos_user.listener.authentication');
$definition->setClass(AuthenticationListener::class);
}
}
The Service Override
namespace arpa3\UserBundle\EventListener;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Security\LoginManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
class AuthenticationListener implements EventSubscriberInterface
{
/**
* #var LoginManagerInterface
*/
private $loginManager;
/**
* #var string
*/
private $firewallName;
/**
* AuthenticationListener constructor.
*
* #param LoginManagerInterface $loginManager
* #param string $firewallName
*/
public function __construct(LoginManagerInterface $loginManager, $firewallName)
{
$this->loginManager = $loginManager;
$this->firewallName = $firewallName;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
// You can disable any of them or all of them as you want
//FOSUserEvents::REGISTRATION_COMPLETED => 'authenticate',
//FOSUserEvents::REGISTRATION_CONFIRMED => 'authenticate',
//FOSUserEvents::RESETTING_RESET_COMPLETED => 'authenticate',
);
}
/**
* #param FilterUserResponseEvent $event
* #param string $eventName
* #param EventDispatcherInterface $eventDispatcher
*/
public function authenticate(FilterUserResponseEvent $event, $eventName, EventDispatcherInterface $eventDispatcher)
{
try {
$this->loginManager->logInUser($this->firewallName, $event->getUser(), $event->getResponse());
$eventDispatcher->dispatch(FOSUserEvents::SECURITY_IMPLICIT_LOGIN, new UserEvent($event->getUser(), $event->getRequest()));
} catch (AccountStatusException $ex) {
// We simply do not authenticate users which do not pass the user
// checker (not enabled, expired, etc.).
}
}
}
Register your Compiler Pass on your main bundle file
namespace arpa3\UserBundle;
use arpa3\UserBundle\DependencyInjection\OverrideServiceCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class arpa3UserBundle extends Bundle {
public function getParent () {
return 'FOSUserBundle';
}
/**
*
* This injects a Compiler Pass that is used to override the automatic login after registration of a new user
* We have done this in order to disable the "by default" behaviour given that only admins can register users
* and logging in into the newly created account automatically is just not a desired behaviour
*
* #param ContainerBuilder $container
*/
public function build ( ContainerBuilder $container ) {
parent ::build( $container );
$container -> addCompilerPass( new OverrideServiceCompilerPass() );
}
}
There are other ways such as overriding the authentication service on your config.yml but the solution above is the cleanest and most maintainable solution I have found.
You are almost there, as you said your listeners are called but the order is not correct, so you need to make your listener be executed before the default one
In order to do that change
FOSUserEvents::REGISTRATION_SUCCESS =>
'onRegistrationSuccess'
to
FOSUserEvents::REGISTRATION_SUCCESS =>
['onRegistrationSuccess',-10],
Notice the -10 there, this changes the priority of the listener.
class RegistrationSuccessEventListener implements EventSubscriberInterface{
private $router;
public function __construct(UrlGeneratorInterface $router){
$this->router = $router;
}
public static function getSubscribedEvents()
{
//this will be called before
return array(
FOSUserEvents::REGISTRATION_SUCCESS => ['onUserRegistrationSuccess', -30],
);
}
/**
* #param FormEvent $event
* When the user registration is completed redirect
* to the employee list page and avoid the automatic
* mail sending and user authentication that happends
*
*/
public function onUserRegistrationSuccess(FormEvent $event){
$url = $this->router->generate('employees_list');
$event->setResponse(new RedirectResponse($url));
}
}
I am using symfony 2.8 with the FOSBundle version
friendsofsymfony/user-bundle dev-master 1f97ccf Symfony FOSUserBundle
according to the output of composer info
In my project (BtoB project), I have a global application with a lot of modules in it.
Each module provides common functionnalities, for all of my clients.
I have also in the root directory a clients folder, in it, I have all clients specificities, in their folder.
Thoses folders, aren't modules. So they are not loaded with Zf2. I usually load those specificities with abstractFactories.
This architecture follow is what I have currently :
- clients
- clientOne
- Invoice
- Cart
- Orders
- clientTwo
- Invoice
- Orders
- clientThree
- Reporting
- module
- Application
- CartModule
- InvoiceModule
- OrdersModule
- Reporting
My clients wants to have some custom views, sometimes, they ask us to provide those views. But my application give a common view for all of them. I have to modify this architecture to load a client view if it exist, or load the common view.
To handle this case I Imagine to have into each clients folder this :
- client
- clientOne
- Invoice
- Cart
- View
- cartView.phtml
- Orders
EDIT :
After some good answers (#AlexP & #Wilt), I tried to implements this solution :
So I have a ClientStrategy; it's factory is Like This :
<?php
namespace Application\View\Strategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Application\View\Resolver\TemplateMapResolver;
use Zend\View\Resolver;
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$map = $serviceLocator->get('config')['view_manager']['template_map'];
$resolver = new Resolver\AggregateResolver();
$map = new TemplateMapResolver($map, $this->clientMap($session->offsetGet('cod_entprim')));
$resolver
->attach($map)
->attach(new Resolver\RelativeFallbackResolver($map));
$viewRenderer->setResolver($resolver);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #return array
*/
public function clientMap($codprim)
{
$clients = array(
21500 => 'clientOne',
32000 => 'clientTwo',
// ..
);
return (isset($clients[$codprim])) ? $clients[$codprim]: false;
}
}
My clientMap method allow me to load my client folder, and views it may have in it like this :
class ClientOne
{
/**
* get The main Code
* #return integer
*/
public function getCodEntPrim()
{
return 21500;
}
/**
* Load all customs views
* #return array
*/
public function customViews()
{
return array(
'addDotations' => __DIR__ . '/Dotations/view/dotations/dotations/add-dotations.phtml',
);
}
/**
* GetName
* #return string
*/
public function getName()
{
return get_class();
}
}
So when it comes to my TemplateMapResolver to do his job I do this :
<?php
namespace Application\View\Resolver;
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving view.
*
* #param string $clientName
*/
protected $clientName;
/**
* Merge nos vues avec celle clients avant de repeupler l'arrayMap global
* #param array $map [description]
*/
public function __construct(array $map, $client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->mergeMap($map);
}
parent::__construct($map);
}
/**
* Merge les map normales avec les map clients, pas propre ?
* #param array $map
* #return array
*/
public function mergeMap($map)
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
$clientMap = $class->customViews();
return array_replace_recursive($map, $clientMap);
}
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
return parent::get($name);
}
/**
* Gets the Client name to use when retrieving view.
*
* #return string
*/
public function getClientName()
{
return $this->clientName;
}
/**
* Sets the Client name to use when retrieving view.
*
* #param mixed $clientName the client name
*
* #return self
*/
public function setClientName($clientName)
{
$this->clientName = $clientName;
return $this;
}
}
I tried a lot of things, this works but somes issues cames up :
My template_path_stack not works anymore, so a lot of my views are broken.
I think this is a complete mess, to do this, that way.
Hard to maintain.
I understand a bit better, how it works, but i'm still unable to implements it the good way.
If you really want to do that (I am not so sure if it is the best way) then you can extend the TemplateMapResolver with your custom logic and set it in your Renderer instance.
Make your custom class:
<?php
Application\View\Resolver
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving template.
*
* #param string $clientName
*/
protected $clientName;
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
if ($this->has($clientName . '_' . $name)) {
return $this->map[$clientName . '_' . $name];
}
if (!$this->has($name)) {
return false;
}
return $this->map[$name];
}
}
And now something like:
$resolver = new TemplateMapResolver();
$resolver->setClientName($clientName);
// Get the renderer instance
$renderer->setResolver($resolver);
You might still have to take care of setting the map in the resolver. Maybe you can just get it from the old resolver? I am not sure... That is for you to find out. This is just to get you on the correct way.
So if you set cart_view as a template it will first try to get client_name_cart_view if not found it sets cart_view.
UPDATE
If you want to take this to the next level, then what you can do is make a custom view model for example ClientViewModel that extends the normal ViewModel class.
The constructor for this ClientViewModel takes both a client and a template name:
new ClientViewModel($client, $template, $variables, $options);
$variables and $options are optional and can be passed to the parent::__construct (constructor of the normal ViewModel)
The next step would be to create a Application\View\ClientStrategy.
This strategy is connected on render event and in this strategy you add a ViewRenderer instance with your custom TemplateMapResolver set. During rendering you can get your client from your ViewModel and find the correct template in your TemplateMapResolver using this client.
More details can be found online, there are examples. Check for example here.
The advantage will be that other views with ViewModel or JsonModel will be rendered as normally, only your ClientViewModel gets a special treatment. Thus you are not breaking your applications default logic.
Requirements
Multiple possible views per client
Default view fallback if client specific view not found
Create a new service, say TemplateProviderService which has a simple interface.
interface ViewTemplateProviderInterface
{
public function hasTemplate($name);
public function getTemplates();
public function setTemplates($templates);
public function getTemplate($name);
public function setTemplate($name, $template);
public function removeTemplate($name);
public function removeTemplates();
}
Inject and hard code the template name in controller classes.
// Some controller class
public function fooAction()
{
$view = new ViewModel();
$view->setTemplate($this->templateProvider->get('some_view_name'));
return $view;
}
Now you can create client specific factories that inject custom template script config into your template provider. All you would then need to do is decide which template provider service you want to inject into your controller.
class ViewTemplateProviderFactory
{
public function __invoke($sm, $name, $rname)
{
$config = $sm->get('config');
if (! isset($config['view_template_providers'][$rname])) {
throw new ServiceNotCreatedException(sprintf('No view template provider config for \'%s\'.', $rname));
}
return new ViewTemplateProvider($config['view_template_providers'][$rname]);
}
}
The key here is ALL view scripts, for all clients, are registered under the 'view_manager' key as normal however the name of the template in the controller never changes.
Edit
You could just use one factory and pull from config (see changes above).
return [
'view_template_providers' => [
'ClientOneTemplateProvider' => [
'some_view_name' => 'name_of_script_1'
],
'ClientTwoTemplateProvider' => [
'some_view_name' => 'name_of_script_2'
],
'ClientThreeTemplateProvider' => [
'some_view_name' => 'name_of_script_3',
],
],
'service_manager' => [
'factories' => [
'ClientOneTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientTwoTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientThreeTemplateProvider' => 'ViewTemplateProviderFactory',
],
],
'view_manager' => [
'template_map' => [
'name_of_script_1' => __DIR__ . 'file/path/to/script',
'name_of_script_2' => __DIR__ . 'file/path/to/script',
'name_of_script_3' => __DIR__ . 'file/path/to/script',
],
],
];
It seems I solved my problem, but i'm not sure it's the good way to do it. So if someone can do better, I let the bounty runs for a better solution, if exists.
Here is what I've done :
/**
* Factory permettant d'établir que les vues client soient chargé si elle existent, avant les vues par défaut.
*/
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$clientList = $serviceLocator->get('Config')['customers_list'];
$clientName = $this->clientMap($session->offsetGet('cod_entprim'), $clientList);
$clientMap = new TemplateMapResolver($clientName);
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #param integer $codprim
* #param array $clientList
* #return array
*/
public function clientMap($codprim, $clientList)
{
return (isset($clientList[$codprim])) ? $clientList[$codprim]: false;
}
}
You can see that my custom TemplateMapResolver needs a clientName, this is for loading custom views. But the most important thing is : I don't create a new Resolver, I just add my Resolver to the list by this line :
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
The second argument means, that this resolver is top priority (Default priority is 1)
My TemplateMapResolver is pretty much simple, the most important thing is this :
public function __construct($client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->getMap();
} else {
$map = array();
}
parent::__construct($map);
}
/**
* Return all custom views for one client
* #param array $map
* #return array
*/
public function getMap()
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
return $class->customViews();
}
My solution, force me to create then a class in my clients folder with the same name of the folder so, if my clientName is TrumanShow i will have an architecture like :
- [clients]
-- [TrumanShow]
--- TrumanShow.php
--- [Cart]
---- [view]
----- [cart]
------ [index]
------- cart-view.phtml
--- [Invoice]
--- [Reporting]
And in this file I will have this function that declare all my custom views :
/**
* Ici nous mettons nos custom views afin de les charger dans le template Map
* #return array
*/
public function customViews()
{
return array(
'cartView' => __DIR__ . '/Cart/view/cart/index/cart-view.phtml',
);
}
So it's possible to do this without break template_path_stack or my others routes. Now I have to call setTemplate method in my Controller, like this :
// code ...
public function cartAction() {
$view->setTemplate('cartView');
return $view;
}
And ZendFramework will check first if a custom view exists in my clients folder, or load the common view if no view is found.
Thanks to #Wilt and #AlexP for their contribution and help.
Don't overcomplicate things. Just set the ViewModel's template before you render it.
$vm = new ViewModel();
$vm->setTemplate( $user_service->getTemplate( $this->getRequest() ) );
return $vm;
Pretty clean if you inject your user into this fictitious user service, and use it to ascertain which template to inject.
The concern of the $user_service should be completely disparate from the concern for your Controller action.
I want to personalize my Symfony project by letting the user choose a city in a selectbox in the top navi. For that I got a query string e.g. ?city=berlin that I fetch in my controllers and filter the results with.
Is there an easy way to keep that query string on every url alive or would you prefer an other solution without a query string? Maybe with cookies?
Thanks for your help!
Better than talking about cookies is the question about stateful or stateless session. Cookies is just the implementation of mapping the client to the session.
Let's say you have a visitor on one city-parametrized page. What do you except your page to look like, when someone copies the url and shares it with others? city is not any personal state, although you mentioned personalized above (e.g. having responsive pages where I can set the font to 120% size or setting higher contrast, would be a personalized configuration I actually don't want to share in the url).
city is part of the state of the page and not the session, thus we want city to be part of the url. Define a prefix route like /{city} and import another yml with that prefix (http://symfony.com/doc/current/book/routing.html#prefixing-imported-routes).
Every time you generate an url with a city you have to set it. You could do this manually or create some CityDecoratedRouter implements RouterInterface getting #router and #request_stack injected and appends the city parameter to all parameter-arrays in generate() calls.
#EvgeniyKuzmin's answer is imho too much magic no one expects. When dealing with those routes having a city parameter it's better to read it in the code, that the routes are treated differently. Of course you also have to define some new city_path function for twig, which uses our CityDecoratedRouter.
If you need to stick user to some condition base on route path (I will advice you use SEO urls instead of GET query) and then use it as stick filter for some behavior on other pages, then you can do such listener:
BaseKernelEvents:
namespace App\CoreBundle\Listener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
abstract class BaseKernelEvents
{
/**
* #var \Symfony\Bundle\FrameworkBundle\Routing\Router
*/
private $router;
/**
* Initializes a new instance of the KernelEvents class.
*/
public function __construct(ContainerInterface $container, Router $router)
{
$this->container = $container;
$this->router = $router;
}
/*
* Store and get value by route param passed to uri to request and session
*/
protected function storeParam(GetResponseEvent $event, $name, $default = 'none')
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
/* #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
// instead of attributes you can get query from request here
$value = $request->attributes->get($name);
if (!$value) {
$value = $session->get($name, $default);
}
return $this->setValues($event, $name, $value);
}
/*
* Set name/value to request context and session
*/
protected function setValues(GetResponseEvent $event, $name, $value)
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
$context = $this->router->getContext();
$session = $request->getSession();
$session->set($name, $value);
$context->setParameter($name, $value);
return $value;
}
}
KernelEvents:
namespace LaMelle\ContentSectionBundle\Listener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use App\CoreBundle\Listener\BaseKernelEvents;
class KernelEvents extends BaseKernelEvents
{
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType())
{
$contentsectionSlug = $this->storeParam($event, 'city');
// DO SOMETHINK BASE ON FILTER
// LIKE CREATE GLOBAL TWIG VAR WITH FILTER VALUE
/* #var \Twig_Environment $globals */
$globals = $this->container->get('twig');
$globals->addGlobal('city', $contentsectionSlug);
}
}
}
So in shown example you will have 'city' be filled from session until you will visit route that change 'city' to other value
Background:
I am trying to conditionally load routes based on the request host. I have a database setup that has hosts in it that map to templates. If a user comes in from the host A and that uses template TA I want to load the routes for that template. If they come in from host B then load the routes for that template (TB).
The reason I have to do this is because each template will share many routes. There are however some unique routes for a given template.
It would be fine to restrict each template routes to a given host, except that there are literally 1000's of hosts.
What I Have Tried:
I have tried a custom route loader as described in the documentation here:
http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
However when i configure the service and try and inject the "#request" the constructor fails because $request is null
services:
acme_demo.routing_loader:
class: Acme\DemoBundle\Routing\ExtraLoader
arguments: ["#request"]
tags:
- { name: routing.loader }
Class:
<?php
namespace: Acme\DemoBundle\Routing;
use Symfony\Component\HttpFoundation\Request;
class ExtraLoader
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
// ...
}
This also doesnt work if I try and switch "#request" for "#service_container" then call
$this->container->get('request');
The closest I got to getting this working was following a guide found here:
http://marcjschmidt.de/blog/2013/11/30/symfony-custom-dynamic-router.html
The problem i have with this on is im trying to use annotation on a controller and i cant seem to get the Symfony\Component\Routing\Loader\AnnotationFileLoader working.
Ok I have finally figured out a working solution, Its a mix of several of the above mentioned guides.
Im using a kernel request listener:
services:
website_listener:
class: NameSpace\Bundle\EventListener\WebsiteListener
arguments:
- "#website_service"
- "#template_service"
- "#sensio_framework_extra.routing.loader.annot_dir"
- "#router"
- "%admin_domain%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 33 }
The Listener:
<?php
namespace NameSpace\WebsiteBundle\EventListener;
use NameSpace\TemplateBundle\Service\TemplateService;
use NameSpace\WebsiteBundle\Service\WebsiteService;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\PropertyAccess\PropertyAccess;
class WebsiteListener
{
/**
* #var WebsiteService
*/
protected $websiteService;
/**
* #var TemplateService
*/
protected $templateService;
/**
* #var AnnotationDirectoryLoader
*/
protected $loader;
/**
* #var Router
*/
protected $router;
/**
* #var string
*/
protected $adminDomain;
public function __construct(WebsiteService $websiteService, TemplateService $templateService, AnnotationDirectoryLoader $loader, Router $router, $adminDomain)
{
$this->websiteService = $websiteService;
$this->templateService = $templateService;
$this->loader = $loader;
$this->router = $router;
$this->adminDomain = $adminDomain;
}
public function loadRoutes()
{
$template = $this->templateService->getTemplateByAlias($this->websiteService->getWebsite()->getTemplate());
$routes = $this->loader->load($template['routes'],'annotation');
$allRoutes = $this->router->getRouteCollection();
$allRoutes->addCollection($routes);
}
public function onKernelRequest(GetResponseEvent $event)
{
try {
$this->websiteService->handleRequest($event->getRequest());
$this->loadRoutes();
} catch(NotFoundHttpException $e) {
if($event->getRequest()->getHost() !== $this->adminDomain){
throw $e;
}
}
}
}
The Key parts of this are:
The Loader - I found "#sensio_framework_extra.routing.loader.annot_dir" in the source code. That the annotation directory loader that symfony uses by default so thats the one that I want to use too. But if you want to use a different loader there are others available.
The Router - This is what i use to get all of the current routes. NOTE that the $allRoutes->addCollection($routes) call is on a seperate line. Im not sure why it makes a difference but calling it all in 1 like was not working.
$template['routes'] is just a namespaces controller reference like you would use to add routing in your routing.yml. Something like: "#NamespaceBundle/Controller"
I am using Symfony 2.4, and according to the Docs, the correct way of retrieving the Request object in the controller is the following:
/**
* #Route("/register/next", name="next_registration_step")
*/
public function nextAction(Request $request = null) {...}
This works as expected. However, if I add a parameter to the controller, $request becomes null at runtime:
/**
* #Route("/register/next/{currentStep}", name="next_registration_step")
*/
public function nextAction(Request $request = null, $currentStep = 0) {...}
How do I work around this issue without using any older-but-deprecated methods for getting the request?
Note: if possible, a solution that does not involve the Request Stack recently introduced to Symfony 2.4 would be great, as it seems like overkill.
This works,
as I think the only difference is that I do not pass = null in parameters declaration
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/hello/{name}", name="_demo_hello")
*/
public function helloAction(Request $request, $name)
{
var_dump($request, $name);die();
In Symfony2 controllers it's not a good Idea to declare default value in the method definition - it should be done in routing definition.
In your case:
/*
*
* #Route("/register/next/{currentStep}", name="next_registration_step", defaults={"currentStep" = 0})
*/
public function next(Request $request, $currentStep) {...}
regards,