I'm trying to redirect from the onKernelController event listener to another controller in my bundle. The redirection succeed but in the $newController the container is NULL so I can't really do any thing with it like rendering pages.
Why is the container NULL when it's been created like this? and how can I inject the container service to the $newController in this case?
Thanks! :)
The final working version of the event listener:
namespace MyApp\MainBundle\EventListener;
use MyApp\MainBundle\Interfaces\UserCheckInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ControllerListener
{
private $resolver;
/**
* #param ControllerResolver $resolver The injected controller_resolver service
*/
public function __construct($resolver)
{
$this->resolver = $resolver;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
{
// not an object but a different kind of callable. Do nothing
return;
}
/* #var $controllerObject Controller */
$controllerObject = $controller[0];
if ( $controllerObject instanceof UserCheckInterface )
{
$newRequest = $event->getRequest()->duplicate(null, null, array('_controller' => 'MyApp\MainBundle\Controller\ErrorController::notLoggedInAction'));
/* #var $newController Controller */
$newController = $this->resolver->getController($newRequest);
$event->setController($newController);
}
}
}
Here is the config.yml
kernel.listener.ControllerListener:
class: MyApp\MainBundle\EventListener\ControllerListener
arguments: [ "#controller_resolver" ]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
The component resolver (Symfony\Component\HttpKernel\Controller\ControllerResolver) does not know anything about the container.
Instead, the Symfony FrameworkBundle provides Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver which is container aware. It's worth taking a look at the code to see where setContainer is being called.
Inject the controller_resolver service into your listener then use it instead of creating a new resolver.
OK... I found the answer...
When creating the $newController like I did the container by default is NULL and need to be set with the container of the original controller like this
$newController[0]->setContainer($controllerObject->get("service_container"));
Related
Trying to register a Doctrine EventSubscriber but nothing is ever actually fired.
I have, on the Entity, in question, set the #ORM\HasLifeCycleCallbacks annotation.
Here's the Subscriber:
<?php
namespace App\Subscriber;
use App\Entity\User;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserPasswordChangedSubscriber implements EventSubscriber
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents()
{
return [Events::prePersist, Events::preUpdate, Events::postLoad];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!$entity instanceof User) {
return null;
}
$this->updateUserPassword($entity);
}
private function updateUserPassword(User $user)
{
$plainPassword = $user->getPlainPassword();
if (!empty($plainPassword)) {
$encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword);
$user->setPassword($encodedPassword);
$user->eraseCredentials();
}
}
}
The part that is making this particuarly frustrating is that this same code and configuration was fine in Symfony 3 whe autowiring was turned off and I manually coded all my services.
However, now, even if I manually code up a service entry for this, in the usual way, still nothing happens.
EDIT:
Here is my services.yaml after trying what suggested Domagoj from the Symfony docs:
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
It didn't work. Interestingly, If I un-implement the EventSubscriber interface, Symfony throws an exception (rightly). Yet my break points in the code are completely ignored.
I've considered an EntityListener, but it cannot have a constructor with arguments, doesn't have access to the Container and I shouldn't have to; this ought to work :/
I ended up figuring this out. The field that I was specifically updating was transient, and therefore Doctrine didn't consider this an Entity change (rightly).
To fix this, I put
// Set the updatedAt time to trigger the PreUpdate event
$this->updatedAt = new DateTimeImmutable();
In the Entity field's set method and this forced an update.
I also did need to manually register the Subscriber in the services.yaml using the following code. symfony 4 autowiring wasn't auto enough for a Doctrine Event Subscriber.
App\Subscriber\UserPasswordChangedSubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
For your first problem, doctrine event subscribers are not autoconfigured/auto-tagged. For the reasons and solutions, you have some responses here.
Personnaly, I just have one Doctrine ORM mapper, so I put this in my services.yaml file :
services:
_instanceof:
Doctrine\Common\EventSubscriber:
tags: ['doctrine.event_subscriber']
You have to register your Event Listener as a service and tag it as doctrine.event_listener
https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html#configuring-the-listener-subscriber
First of all, I have to say that I have been seeing answers and documentation for several days but none of them answer my question.
The only and simple thing I want to do is to use the twig service as a global service in a BaseController.
This is my code:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use App\Service\Configuration;
use App\Utils\Util;
abstract class BaseController extends Controller
{
protected $twig;
protected $configuration;
public function __construct(\Twig_Environment $twig,Configuration $configuration)
{
$this->twig = $twig;
$this->configuration = $configuration;
}
}
Then in all my controllers extend the twig and configuration service, without having to inject it again & again.
//...
//......
/**
* #Route("/configuration", name="configuration_")
*/
class ConfigurationController extends BaseController
{
public function __construct()
{
//parent::__construct();
$this->twig->addGlobal('menuActual', "config");
}
As you can see the only thing I want is to have some services global to have everything more organized and also to create some global shortcuts for all my controllers. In this example I am assigning a global variable to make a link active in the menu of my template and in each controller I have to add a new value for menuActual, for example in the UserController the variable would be addGlobal('menuActual', "users").
I think this should be in the good practices of symfony which I don't find :(.
Having to include the \Twig_Environment in each controller to assign a variable to the view seems very repetitive to me. This should come by default in the controller.
Thanks
I've had that problem as well - trying to not have to repeat a bit of code for every controller / action.
I solved it using an event listener:
# services.yaml
app.event_listener.controller_action_listener:
class: App\EventListener\ControllerActionListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
#src/EventListener/ControllerActionListener.php
namespace App\EventListener;
use App\Controller\BaseController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class ControllerActionListener
*
* #package App\EventListener
*/
class ControllerActionListener
{
public function onKernelController(FilterControllerEvent $event)
{
//fetch the controller class if available
$controllerClass = null;
if (!empty($event->getController())) {
$controllerClass = $event->getController()[0];
}
//make sure your global instantiation only fires if the controller extends your base controller
if ($controllerClass instanceof BaseController) {
$controllerClass->getTwig()->addGlobal('menuActual', "config");
}
}
}
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 was wondering if there is away to call a function after the user login.
Here is the code I want to call:
$point = $this->container->get('process_points');
$point->ProcessPoints(1 , $this->container);
You can find the events FOSUserBundle fires in the FOSUserEvents class. More specifically, this is the one you are looking for:
/**
* The SECURITY_IMPLICIT_LOGIN event occurs when the user is logged in programmatically.
*
* This event allows you to access the response which will be sent.
* The event listener method receives a FOS\UserBundle\Event\UserEvent instance.
*/
const SECURITY_IMPLICIT_LOGIN = 'fos_user.security.implicit_login';
The documentation for hooking into those events can be found on the Hooking into the controllers doc page. In your case, you will need to implement something like this:
namespace Acme\UserBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
/**
* Listener responsible to change the redirection at the end of the password resetting
*/
class LoginListener implements EventSubscriberInterface
{
private $container;
public function __construct($container)
{
$this->container = $container;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::SECURITY_IMPLICIT_LOGIN => 'onLogin',
SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
);
}
public function onLogin($event)
{
// FYI
// if ($event instanceof UserEvent) {
// $user = $event->getUser();
// }
// if ($event instanceof InteractiveLoginEvent) {
// $user = $event->getAuthenticationToken()->getUser();
// }
$point = $this->container->get('process_points');
$point->ProcessPoints(1 , $this->container);
}
}
You should then define the listener as a service and inject the container. Alternatively, you could inject just the service you need instead of the whole container.
services:
acme_user.login:
class: Acme\UserBundle\EventListener\LoginListener
arguments: [#container]
tags:
- { name: kernel.event_subscriber }
There is also another method which involves overriding the controller, but as noted in the documentation, you have to duplicate their code so it's not exactly clean and bound to break if (or rather, when) FOSUserBundle is changed.
I'm trying to work out the best way of handling custom error pages within Symfony2.This includes 500 and 404's etc.
I can create my own custom templates (error404.html.twig etc) and render these out fine, the issue is , the app requires a few variables be passed into the base template for the page to remain consistent. Using the built in exception handler results in required variables not being available.
I have successfully setup a custom Exception Event Listener, and registered it as a service:
namespace MyCo\MyBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\TwigBundle\TwigEngine;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class MyErrorExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// We get the exception object from the received event
$exception = $event->getException();
if($exception->getStatusCode() == 404)
{
//$engine = $this->container->get('templating');
//$content = $engine->render('MyBundle:Default:error404.html.twig');
//return $response = new Response($content);
/* Also Tried */
//$templating = $this->container->get('templating');
//return $this->render('MyBundle:Default:index.html.twig');
$response = new Response($templating->render('MyBundle:Exception:error404.html.twig', array(
'exception' => $exception
)));
$event->setResponse($response);
}
}
}
This doesn't work , as :$container is not available , meaning I cannot render my custom page.
So two questions really , is this the correct way to handle custom error pages, or should I pass the response off to a controller? If so , whats the best way of doing that?
If this is correct , how can I make the templating engine available within my Listener ?
You should add into yours Listener
/**
*
* #var ContainerInterface
*/
private $container;
function __construct($container) {
$this->container = $container;
}
How do you register your Listener?
You should register Listener like Service
Like that
core.exceptlistener:
class: %core.exceptlistener.class%
arguments: [#service_container]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 200 }
The best way is don't use service_container.
The best way is register only necessary services.
Like that
/**
*
* #var Twig_Environment
*/
private $twig;
function __construct($twig) {
$this->twig = $twig;
}
The way I did to solve similar issue is: I dont know if it can help you out.
const GENERIC_CODE = 550;
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof RedirectableException) {
$request = $event->getRequest();
$url = $exception->getUrl($this->_authentication['base']['url']);
$response = new Response();
$response->setStatusCode(self::GENERIC_CODE, AuthenticationException::GENERIC_MESSAGE);
$response->setContent($url);
$event->setResponse($response);
}
}