Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Interrupt Symfony with an Event Subscriber

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Hey guys! Welcome to a series that we're calling: Journey to the Center of Symfony! In this first part, we'll be talking about the deep, dark core piece called the HttpKernel, a wondrous component that not only sits at the heart of Symfony, but also at the heart of Silex, Drupal 8, PhpBB and a lot of other stuff. How is that possible? We'll find out! And this stuff is really nerdy, so we're going to have some fun.

Getting the Project Running

I already have the starting point of our app ready on my computer. You can download this right on the screencast page. I've already run composer install, I've already created my database, I've already created my schema: I won't show those things here because you guys are a bit more of experts. We do have fixtures, so let's load those.

Now let's use the built-in PHP web server to get our site running.

Perfect!

So in true Journey to the Center of the "Symfony" theme, we're going to talk about dinosaurs. I've already created an app, which has 2 pages. We can list dinosaurs - these are coming out of the database - and if we click on one of them, we go to the show page for that dinosaur.

Big Picture: Request-Route-Controller-Response

No matter what technology or framework we're using, our goal is always to start with a request and use that to create a response. Everything in between those 2 steps will be different based on your tech or framework. In our app, and in almost every framework, two things that are going to be between the request and response are the route and controller. In this case, you can see our homepage has a route, our function is a controller, and our controller returns a Response object::

... lines 1 - 10
/**
* @Route("/", name="dinosaur_list")
*/
public function indexAction()
{
$dinos = $this->getDoctrine()
->getRepository('AppBundle:Dinosaur')
->findAll();
return $this->render('dinosaurs/index.html.twig', [
'dinos' => $dinos,
]);
}
... lines 24 - 42

And we have the same thing down here with the other page: it has a route, a controller, and that returns a response::

... lines 1 - 24
/**
* @Route("/dinosaurs/{id}", name="dinosaur_show")
*/
public function showAction($id)
{
$dino = $this->getDoctrine()
->getRepository('AppBundle:Dinosaur')
->find($id);
if (!$dino) {
throw $this->createNotFoundException('That dino is extinct!');
}
return $this->render('dinosaurs/show.html.twig', [
'dino' => $dino,
]);
}

So what we're going to look at is how that all works. Who actually runs the router? Who calls the controller? How do events work in between the request-response flow?

But before we dive into that, what we're going to do first is create an event listener and hook into that process. Then we'll be able to play with that event listener as we dive into the core of things.

The Best Parts of the Web Profiler

I'm going to open up the profiler and go to the timeline. This is going to be our guide to this whole process. This shows everything that happens between the request and the response. Even if you don't understand what's happening yet, after we go through everything, this is going to be a lot more interesting. You can already see where our controller is called, and under the controller you can see the Twig template and even some Doctrine calls being made.

Before and after that, there are a lot of event listeners - you notice a lot of things that end in the word Listener. That's because most of the things that happen between the request and the response in Symfony are events: you have the chance to hook into them with event listeners.

In fact, one other tab I really like on here is the Events tab. You can see there's some event called kernel.request. Maybe you already understand what that means, maybe you don't, but you will soon. There's another event called kernel.controller with listeners and several other events. We're going to see where these events are dispatched and why you would add a hook to one versus another.

Creating an Event Subscriber/Listener

Let's create a listener on that kernel.request event! In my AppBundle, I'll create a new directory called EventListener and a new class. Inside this event listener, we're going to read the User-Agent header off the request and do some things with that. So I'll call this UserAgentSubscriber::

<?php
namespace AppBundle\EventListener;
class UserAgentSubscriber
{
}

If you want to hook into Symfony, there are 2 ways to do it: with a listener or a subscriber. They're actually exactly the same, the only difference is where you configure which events you want to listen to.

I'm going to create a subscriber here because it's a little more flexible. So UserAgentSubscriber needs to implement EventSubscriberInterface::

... lines 1 - 2
namespace AppBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 9 - 19
}

Notice that it added the use statement up there. And we're going to need to implement 1 method which is getSubscribedEvents. What this is going to return is a simple array that says: Hey, apparently there's some event whose name is kernel.request - we don't necessary know why it's called or what it does yet - but when that event happens, I want Symfony to call this onKernelRequest function, which we're going to put inside of this class. For now, let's just put a die('it works');::

... lines 1 - 6
class UserAgentSubscriber implements EventSubscriberInterface
{
public function onKernelRequest()
{
die('it works');
}
public static function getSubscribedEvents()
{
return array(
'kernel.request' => 'onKernelRequest'
);
}
}

Cool! The event subscriber is ready to go. No, Symfony doesn't automatically know this class is here or automatically scan the codebase. So to get Symfony to know that there's a new UserEventSubscriber that wants to listen on the kernel.request event, we're going to need to register this as a service.

Registering the Subscriber/Listener

So I'm going to go into app/config/services.yml and clear the comments out. And we'll give it a short, but descriptive name - user_agent_subscriber, the name of the service doesn't really matter in this case. There are no arguments yet, so I'll just put an empty array. Now in order for Symfony to know this is an event subscriber, we'll use something called a tag, and set its name to kernel.event_subscriber:

... lines 1 - 5
services:
user_agent_subscriber:
class: AppBundle\EventListener\UserAgentSubscriber
tags:
- { name: kernel.event_subscriber }

Now, that tag is called a dependency injection tag, which is really awesome, really advanced and really fun to work with inside of Symfony. And we're going to talk about it in a different part of this series. With just this configuration, Symfony will boot, it'll know about our subscriber, and when that kernel.request event happens, it should call our function.

Sweet!

Logging Something in the Subscriber

Now inside of onKernelRequest, let's do some real work. For now, I want to log a message. I'm going to need the logger so I'll add a constructor and even type hint the argument with the PSR LoggerInterface. And I'll use a little PHPStorm shortcut to create and set that property for me::

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 7
class UserAgentSubscriber implements EventSubscriberInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 16 - 27
}

Now in our function, we'll log a very important message::

... lines 1 - 16
public function onKernelRequest()
{
$this->logger->info('Yea, it totally works!');
}
... lines 21 - 29

And of course this isn't going to work unless we go back to services.yml and tell Symfony: Hey, we need the @logger service:

... lines 1 - 5
services:
user_agent_subscriber:
class: AppBundle\EventListener\UserAgentSubscriber
arguments: ["@logger"]
tags:
- { name: kernel.event_subscriber }

Cool!

Let's refresh! It works, and if we click into the profiler, one of the tabs is called "Logs", and under "info" we can see the message. So this is already working, and if we go back to the Timeline and look closely, we should see our UserAgentSubscriber. And it's right there. Also, if we go back to the events tab, we see the kernel.request with all of its listeners. And if you look at the bottom, you see our UserAgentSubscriber on that list too.

So we're hooking into that process already, even if we don't understand what's going on with it.

Every Listener Gets an Event Object

Whenever you listen to any event - whether it's one of Symfony's core events or it's an event from a third-party bundle you installed, your function is passed an $event argument. So, we'll add $event. The only trick is that you don't automatically know what type of object that is, because every event you listen to is going to pass you a different type of event object.

But no worries! I'm going to use the new dump() function from Symfony 2.6::

... lines 1 - 16
public function onKernelRequest($event)
{
dump($event);
$this->logger->info('Yea, it totally works!');
}
... lines 22 - 30

Let's go back a few pages, refresh, and the dump function prints that out right in the web debug toolbar. And we can see it's dumping a GetResponseEvent object. So that's awesome - now we know what type of object is being passed to us. And that's important because every event object will have different methods and different information on it.

Let's type-hint the argument. Notice I'm using PHPStorm, so that added a nice use statement to the top - don't forget that::

... lines 1 - 6
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
... lines 8 - 17
public function onKernelRequest(GetResponseEvent $event)
{
... lines 20 - 23
}
... lines 25 - 33

What I want to do is get the User-Agent header and print that out in a log message. Fortunately, this getResponseEvent object gives us access to the request object. And again, every event you listen to will give you a different event object, and every event object will have different methods and information on it. It just happens to be that this one has a getRequest method, which is really handy for what we want to do. Now I'll just read the User-Agent off of the headers, and log a message::

... lines 1 - 17
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$userAgent = $request->headers->get('User-Agent');
$this->logger->info('Hello there browser: '.$userAgent);
}
... lines 25 - 33

Let's try it! I'll get back into the profiler, then to the Logs... and it's working perfectly.

Even if we don't understand everything that's happening between the request and response, we already know that there are these listeners that happen. But next, we're going to walk through the code that handles all of this.

Leave a comment!

50
Login or Register to join the conversation
Tavo Avatar

Hi, like always, nice video; what are the differences between a CommandBus and an EventDispatcher in Symfony?

Reply

Hey Gustavo,

They are similar but, but CommandBus is something related to Symfony Messenger component and is used to dispatch *messages*, see https://symfony.com/doc/cur... . While EventDispatcher is a standalone Symfony component and is used to dispatch events. It provides tools that allow your application components to communicate with each other by dispatching events and listening to them.

Cheers!

1 Reply

Hello, is there a way to download this course code from console in linux ?

Cheers

Reply

Hey Diego!

Not currently - you're the first to ask! But that would be really (geeky) cool - it's something I'm going to keep thinking about :).

Cheers!

1 Reply
Default user avatar
Default user avatar Shairyar Baig | posted 5 years ago

very nice tutorial. What is the difference between Events and Dependency Injection? Or similarity? Would it make sense to create enents to generate email. Forexampl if a user register on your website an event should be dispatched to send a welcome email. Can events be used or should they be used for a small tasks like these to keep the controller thin?

Reply

Hey!

So even though they work together in Symfony, think of events and dependency injection as two separate things. Events are no different than in JavaScript. For example, I can register a "listener" to "listen" on the "click" event of a button (via jQuery) with


$('.some-button').on('click', function() { console.log('this is a listener!'); });

In Symfony, you register your event listeners as services, but technically, event listeners are flat functions. So, try to keep the ideas separate :).

To your question - you absolutely could create an event to generate an email after a user registers. In fact, FOSUserBundle does something like this: they dispatch a "registration success" event after a successful registration. You could add a listener to this in order to send the user an email: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Controller/RegistrationController.php#L59. That's a perfect use-case.

And yes, this could be used to keep your controllers shorter. However, be a little careful: if you dispatch a lot of events, it makes your code ver modular and your controllers short. BUT, it also makes your code harder to follow: if I see an event dispatched in a controller, I can't easily see who listens to that or what those listeners do. But if I see the code right in the controller, OR a method call out to another service (which then sends an email and anything else), then the code is less abstract and easier to understand.

Make sense? Cheers!

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago

Thanks, makes more sense now. How can I share data submitted via a form from controller to an event so the event method can make use of that to perform its task.

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago

I was able to set it up fine thanks to this tutorial, what i am stuck at understanding now is what to do with the data being fetched at kernel.request. Using the dump() function I can see the results so the query is working fine in my event listener. How can i access this data now in any controller or twig template?

Reply

Hey again!

I see two comments, which I think are both asking nearly the same question - a good question :).

1) If you're dispatching your own events and want to pass the listeners some data (e.g. the data submitted via a form), you'll create a brand new Event object (that extends Symfony's Event), add any properties you need on it set that data on the custom event object before dispatching it. Check out the FOSUserBundle link from above - they do this exact thing. You're always free just to create a generic Symfony Event object, but if you're passing some extra data, create your own.

2) It sounds like you're querying for something via a listener in kernel.request and that you want to make this value available to all of your controllers and maybe templates too. There are a few ways of doing this. The most straightforward is to store this value on some service - fortunately I have an example of something like this handy :) https://knpuniversity.com/screencast/question-answer-day/symfony2-dynamic-subdomains#determining-the-site-automatically-with-an-event-listener.

If you want to get really fancy, you could make this value available as an argument in any controller in your system by modifying the request attributes. This is easy - but quite advanced, and I only mention it because that advanced stuff is exactly what this tutorial is about :).


// in the listener
public function onKernelRequest(GetResponseForEvent $event)
{
    $someObject = // ... make a query
    $request = $event->getRequest();
    $request->attributes->set('myCoolVar', $someObject);
}


// now in ANY controller in the system
public function showAction($id, $myCoolVar)
{
    // ...
}

How's that sound? :)

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago | edited

It sounds great, I was able to query database for the data i needed in my listener and I was able to set the outcome as twig global variable so i can now access that on every page, this is what my listener looks like, this is my first listener ever.....


namespace ProjectEverest\CoreBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class StudentApplicationSubscriber extends ContainerAware implements EventSubscriberInterface
{
    protected $em;
    protected $twig;

    function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public static function getSubscribedEvents()
    {

        return array(
            'kernel.request' => 'onKernelRequest'
        );

    }

    public function onKernelRequest()
    {

        //get details of logged in user
        $get_user_details = $this->container->get('security.token_storage')->getToken()->getUser();

        //make sure to pull information when user is logged in
        $securityContext = $this->container->get('security.authorization_checker');
        if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
            // authenticated REMEMBERED, FULLY will imply REMEMBERED (NON anonymous)

            //get user id of logged in user
            $userId = $get_user_details->getId();

            //check if the user added a photo
            $get_user_image = $this->em->getRepository('CoreBundle:ProfileImage')->findBy(array('userId'=>$userId));

            if($get_user_image){
                $profile_image_exist  = 'yes';
            }else{
                $profile_image_exist  = 'no';
            }
            // assign the value of photo to twig global variable
            $this->container->get('twig')->addGlobal('profile_image_exist', $profile_image_exist);

            //check if user has filled the application
            $get_user_application = $this->em->getRepository('CoreBundle:Apply')->findBy(array('userid' => $userId));

            if($get_user_application){
                $user_application_exist = 'yes';
            }else{
                $user_application_exist = 'no';
            }
            // assign the value of application to twig global variable
            $this->container->get('twig')->addGlobal('user_application_exist', $user_application_exist);
        }
    }
}

I did run into a scenario where this event was being dispatched even on route that did not exist or even when a user was not logged in so I added the



if ($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {...} 

to stop the query from running for non logged in user, but how do i stop this from running on routes that dont exist?

Reply

Hey!

Nice work - happy it's working! So:

1) "how do i get this to stop running on routes that don't exist?". The best answer is: you don't. Basically, why do you care? If the route doesn't exist, then it won't matter anyways. I think that's over-optimizing. But also, I *think* that this won't run anyways - the layer that runs the router is earlier than this listener, and when it fails to find a route, it throws an Exception and stops the rest of the listeners.

2) I see you're extending ContainerAware and this is giving you a `$this->container` property. Where did you get this code from? I don't think this is a core feature of Symfony itself - do you have a bundle installed? Basically, this is "ok", but there's a better way. You're already "injecting" the EntityManager via the __construct function. You should also add a few more arguments: $tokenStorage for security.token_storage, $authChecker for security.authorization_checker and $twig for twig. You would then set these on properties (just like you do with $em), update the arguments key in your services.yml to pass these (just like you did with the entity manager service), and then reference the properties directly (i.e. $this->twig instead of $this->container->get('twig')). Then, you won't need the ContainerAware class - and I'm not sure what's making that work anyways :). But more importantly, you'll be using "dependency injection" properly: passing your class all the other services it needs.

Cheers!

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago | edited

Many thanks for the feedback, I got the code from searching answers online following different tutorials. I am not using any third party bundles yet just a plain simple Symfony framework.

1) Reason I asked the route query was I noticed when the route does not exist, instead of seeing the 404 error i see


FatalErrorException in StudentApplicationSubscriber.php line 36:Error: Call to a member function getUser() on null

The above error is because of using getUser() in event listener, just to check if i remove the event listener i do see the 404 error, how do i fix this?

2) Ok so following your instruction about getting rid of ContainerAware, i did that and it is working perfectly fine. this is how my services.yml looks like now


services:
    student_application_subscriber:
        class: ProjectEverest\CoreBundle\EventListener\StudentApplicationSubscriber
        arguments:
            - @doctrine.orm.entity_manager
            - @security.token_storage
            - @security.authorization_checker
            - @twig
        tags:
            - { name: kernel.event_subscriber 

This is how my event listener looks like now


namespace ProjectEverest\CoreBundle\EventListener;

use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class StudentApplicationSubscriber implements EventSubscriberInterface
{
    protected $em;
    protected $twig;
    protected $tokenStorage;
    protected $authChecker;

    function __construct(EntityManager $em, $tokenStorage, $authChecker, $twig)
    {
        $this->em = $em;
        $this->twig = $twig;
        $this->tokenStorage = $tokenStorage;
        $this->authChecker = $authChecker;
    }

    public static function getSubscribedEvents()
    {

        return array(
            'kernel.request' => 'onKernelRequest'
        );

    }

    public function onKernelRequest()
    {

        //get details of logged in user
        $get_user_details = $this->tokenStorage->getToken()->getUser();

        //make sure to pull information when user is logged in
        if ($this->authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {

            //get user id of logged in user
            $userId = $get_user_details->getId();

            //check if the user added a photo
            $get_user_image = $this->em->getRepository('CoreBundle:ProfileImage')->findBy(array('userId' => $userId));

            if ($get_user_image) {
                $profile_image_exist = 'yes';
            } else {
                $profile_image_exist = 'no';
            }
            // assign the value of photo to twig global variable
            $this->twig->addGlobal('profile_image_exist', $profile_image_exist);

            //check if user has filled the application
            $get_user_application = $this->em->getRepository('CoreBundle:Apply')->findBy(array('userid' => $userId));

            if ($get_user_application) {
                $user_application_exist = 'yes';
            } else {
                $user_application_exist = 'no';
            }
            // assign the value of application to twig global variable
            $this->twig->addGlobal('user_application_exist', $user_application_exist);
        }

    }

}

Many thanks for all the help

Reply

Hey!

Oh yes yes, I know what's happening. Here's how it works:

A) the RouterListener runs. There is no route, so it throws an exception
B) The security system (which is also a listener) is never called, so security is never initialized. Your listener is also never called
C) To render the 404 page, a "sub request" is made. This calls all the listeners again. The security system is programmed to not run this time however, so there is still no user. BUT, your listener does run again.

To fix this, be a little more careful when getting the user (especially in a listener). I'd do:


if (!$token = $this->tokenStorage->getToken()) {
    // possible only if the security system didn't run, usually due to an error like a 404
    return;
}


$user = $token->getUser();
if (!is_object($user)) {
    // there is no user - the user may not be logged in
    return;
}

This kind of code is only needed in code that may be run somewhere when an error has happened (listeners, or your exception template, for example). It's kind of an ugly detail.

And nice work on the service setup - that's exactly what I had in mind! I still have no idea how the ContainerAware things was even working - I see nothing in the core code that would do that...

Cheers!

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago | edited

Thank you so much, by using the following piece of code I was able to see the 404 error


if (!$token = $this->tokenStorage->getToken()) {
    // possible only if the security system didn't run, usually due to an error like a 404
    return;
}

just wondering if i still need to use (my code works fine if i dont use it, i dont see any error)



$user = $token->getUser();
if (!is_object($user)) {
    // there is no user - the user may not be logged in
    return;
}
Reply

Yea, you probably don't need that part (you're right that the other check should be enough) :)

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago

Just wanted to say thanks. I will now be digging into dependency injection

Reply
JuanLuisGarciaBorrego Avatar
JuanLuisGarciaBorrego Avatar JuanLuisGarciaBorrego | posted 5 years ago

Hello,
Do not fail to understand the difference between listener vs subscriber?
Could you clarify my doubt? Thank you very much Wea Ryan!

Reply

Hi Juan!

They are really the same thing :). I will say it again: a listener and a subscriber have the *same* purpose. Both have a function that Symfony calls when an event occurs.

So, what is the difference? Well, somewhere, you need to say "Hey Symfony, please call onKernelRequest when the kernel.request event happens". Listeners and subscribers keep this "configuration" in a different place:

Listener: This configuration is stored in the service definition, under the "event" and "method" options of the tag

Subscriber: This configuration is stored right inside the class, inside the getSubscribedEvents function.

Everything that can be done with a listener can be done with a subscriber, and vice-versa :). So, when you are choosing between them, the choice is entirely up to you. I tend to prefer subscribers, because I like to be able to look inside the class and see all my information in one spot.

Does that help?

2 Reply
JuanLuisGarciaBorrego Avatar
JuanLuisGarciaBorrego Avatar JuanLuisGarciaBorrego | weaverryan | posted 5 years ago

Now a little clearer. In the event we put subscriber execution priorities and in no event listener. I think it's not important priorities for most cases ....

Respect to events as kernel.terminate there any way to make the event run by the controller?

Thank you very much for your answer.

Reply

Hi Juan!

Yes, what you say above about listeners and subscribers is true. But also, you can specify a priority for a listener. But for a listener, this priority lives with your service definition:


services:
    my_listener:
        class: AppBundle\EventListener\SomeListener
        arguments: []
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 100 }

So again, a listener and a subscriber have the same abilities :).

<blockquote>Respect to events as kernel.terminate there any way to make the event run by the controller?</blockquote>

I don't think I understand. The kernel.terminate event happens after your controller. In other words, all listeners to the kernel.terminate event are called after your controller automatically. What do you want to accomplish with this event?

Cheers!

Reply
JuanLuisGarciaBorrego Avatar
JuanLuisGarciaBorrego Avatar JuanLuisGarciaBorrego | weaverryan | posted 5 years ago

Ryan thank you very much!
Now I understand.
Respect to events as kernel.terminate, I intend to send an email or pdf after sending the response to the user.

greetings and thanks Ryan!

Reply

Hi Juan!

Ah, I understand! So first, about sending an email after the response was sent, this is already a built-in feature called "memory spooling": http://symfony.com/doc/current/cookbook/email/spool.html#spool-using-memory. It works exactly like you expect: you send an email like normal, but it doesn't send immediately. Instead, a listener on kernel.terminate sends the email.

But, what if this weren't a feature and you needed to build it yourself? The process would look something like this (it is a little bit more difficult, but I hope you will understand the flow):

A) Create some class that helps you send emails - e.g. a MailerManager class - and give it two methods: queueEmail($message) and sendAllQueuedEmails(). Register this as a service (named, for example, "app.mailer_manager". The queueEmail() wouldn't actually send the email - it would just set it on some private property in the class (e.g. $queuedMessages). Then, the sendAllQueuedEmails() will eventually loop over this property and use the normal mailer service to send these.

B) In your controller, instead of getting the "mailer" service and using it to send the email directly, you would instead get your "app.mailer_manager" service and use your queueEmail() method:


public function someAction()
{
    // ...
    $this->get('app.mailer_manager')->queueEmail($someMessageVariable);
}

C) Finally, create a new event listener class and make it listen on kernel.terminate. You'll inject (via the constructor) the MailerManager service into this, and the code would look something like this:


class MailSenderListener
{
    private $mailerManager;


    public function __construct(MailerManager $mailerManager)
    {
        $this->mailerManager = $mailerManager;
    }


    public function onKernelTerminate(PostResponseEvent $event)
    {
        $this->mailerManager->sendAllQueuedEmails();
    }
}

The MailerManager::sendAllQueuedEmails() will loop over the queued messages and use the real mailer service to send those (so, the real mailer service would be passed into the constructor of MailerManager).

How does that sound? So you don't call something in kernel.terminate directly from your controller. Instead, that listener will always run. Your job is to make "store" information somewhere so that when your listener executes, it knows what you want it to do.

Cheers!

Reply
JuanLuisGarciaBorrego Avatar
JuanLuisGarciaBorrego Avatar JuanLuisGarciaBorrego | weaverryan | posted 5 years ago

Thanks so much Ryan!
You're very friendly.

Pd. I am eager to begin the tutorial on Continuous Deployment With Symfony2

Greetings!!! =)

Reply
Default user avatar
Default user avatar argy_13 | posted 5 years ago

Hello guys,
i have a problem with loading the dinosaurs images! Probably the

src="{{ dino.imageUrl }}"

in dinosaurs\show.html.twig
doesn t work. What can i do for this?

Thanks in advance!

Reply

Hey argy_13 ,

Let's try to debug things: write "{{ dump(dino.imageUrl) }}" somewhere in the template and see what is this path. Then, go to your filesystem and find this image. You need to compare the path you dumped and the real path of the image. Probably you need to tweak this path a bit or add some nested directories to the relative web path.

Cheers!

1 Reply
Default user avatar

Thank you, Victor for your quick reply! And just to say to anybody alse that will see the same thing from now on, it s because these images have disappeared from the url. If they want they can put inside the databases other Url or path for images!

Reply

Hey argy_13 ,

Thanks for sharing the reason with others!

Cheers!

1 Reply
Default user avatar
Default user avatar Shairyar Baig | posted 5 years ago

Hi Ryan,

I was wondering if it is possible via event listener or some other way in Symfony to know if an email has been sent. Reason I am asking this is because I am creating files for users on server and this file gets sent via email as an attachment. Once the email has been sent this file is no longer required so I want to delete it.

Reply

Hi Shairyar!

Cool question. The answer depends on *how* your email is sent:

1) If you are sending the email in your Symfony code somewhere (maybe via an app/console script or some other Symfony controller - it doesn't matter), then you could of course just delete the file right at that same moment. Or, more elegantly, you could dispatch your own event - e.g. "file.email_sent" and you would send it some new event object (e.g. FileEmailSentEvent) that has information about which email was just sent and which file should be deleted. Then, you could register a listener to this event to delete the file. The pro is that this makes your code very decentralized. The con is that it makes your code a little bit harder to understand: I can't immediately see what is happening when I look at your original code.

2) If some external service is sending the email (e.g. Mandrill), then you would need to look into webhooks that the service can send you when *it* completes sending. In that case, Symfony events wouldn't be involved.

Let me know if I somewhat answered your question... or if I misunderstood :).

Cheers!

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago

Hi,

I am using SwiftMailer to sent email, I did some search on google and came across an option of "unlink(filename)" but using this option leads to an error may be the code deletes the file before the uploading is ever completed.

In another area of the app I am allowing people to generate files and download them, but there I am using BinaryFileResponse() which gives me an access to deleteFileAfterSend() and that seems to work perfectly fine, it deletes the file after sending it as a response.

I kind of want to avoid creating the event listener just to delete the file, I was hoping SwiftMail or Symfony had some other way for us to know that the file is attached now and can be deleted safely.

Reply

Hey Shairyar!

Ah, I see! So, depending on your configuration, Swiftmailer may send your email immediately (meaning, you can just do an unlink on the line after calling send) *or* will wait until the very end of the request (after the response has been sent) and *then* send the email. The second is the default in the Symfony Standard Edition - it's called "memory spooling". What happens is that Swiftmailer stores the Message object when you send it. Then, it has a listener on the kernel.terminate event, which actually sends any queued (spooled) messages: https://github.com/symfony/...

So, this doesn't help you yet, except that the easiest solution is to turn off memory spooling and allow the message to be sent synchronously (so that you can unlink the file on the next line). The user will receive a slight performance degradation on page loads where you send an email... which is probably not very many.

Otherwise, the solution is more complicated. Unfortunately, Swiftmailer doesn't dispatch an event (that I know of) after it sends spooled emails. That's *really* what you need. However, check out this bundle: it appears to add that hook: https://github.com/TDMobili.... If I'm correct, then you would register a listener on the "tdm.swiftmailer.mailer.post_send_cleanup" event and be able to delete the file.

Let me know if this helps!

Reply
Default user avatar
Default user avatar Mert Simsek | posted 5 years ago

Hi,

I want to send real time data to Twig file. In the Internet research I saw the StreamedResponse class. Is it different from normal ajax request? or is there another way to transfer data without having to render the twig file?

public function streamedResponseAction(Request $request)
{
$response = new StreamedResponse();
$response->setCallback(function () {
echo 'Hello World';
flush();
sleep(3);
echo 'Hello World';
flush();
});

return $response;

}

Reply

Yo Mert Simsek!

I just replied to your other thread here: https://knpuniversity.com/s...

Basically, there is no difference between StreamedResponse and some normal Response: you would make a traditional AJAX call to get either of these. A StreamedResponse is just a mechanism for returning a normal response. For real-time data, you need a different solution. Oh, and btw, we use Pusher for this functionality: we publish messages to it and it has an easy JS API where we can subscribe to those messages.

Cheers!

Reply
Default user avatar
Default user avatar Vivek Sharma | posted 5 years ago

I want to know about Event Dispatcher,Event Listner and Event Subscriber ,Actually i wan to create a Dispatcher exactly same as how symfony has provided in the document i am confused how to implement this document in symfony project .

Please give me some idea.

Symfony Docs Reference:- https://symfony.com/doc/cur...

Reply

Yo Vivek Sharma!

So, first let me make sure I understand correctly :). Usually, people interact with the event dispatcher in a Symfony project by "listening" (or subscribing) to events. That's what we do in this tutorial. Basically, this is when you want to hook into some functionality (e.g. call my function when Symfony does XX).

However, it sounds like you might want to do something different: it sounds like you want to actually dispatch your own events, so that other parts of your application can listen/subscribe to them. Is that correct? If that's true, read this section: https://symfony.com/doc/current/components/event_dispatcher.html#creating-and-dispatching-an-event. The only difference is that - since you're using Symfony - you won't need to create the $dispatcher object. Instead, dispatching your event from a controller would look like this:


// in some controller
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
// ...

public function someAction(EventDispatcherInterface $dispatcher)
{
    $order = new Order();
    // ...

    // create the OrderPlacedEvent and dispatch it
    // obviously, you will create your *own* Event object and event name, and use it here
    $event = new OrderPlacedEvent($order);
    $dispatcher->dispatch(OrderPlacedEvent::NAME, $event);
}

Let me know if that helps!

Reply
Default user avatar

My requirement is that once a user logged in i want to give a pop up msg like login successfully.But i want to achieve it using Dispatcher.I want to create my own custom events.
My doubt is what shall i provide in Order class in docs they have provided something like this:-
// the order is somehow created or retrieved
$order = new Order();

My Files are as following:-

Event class :-
class LoginEvent extends Event
{
const LOGIN = 'login.successfull';

protected $login;

public function __construct(Login $login)
{
$this->login = $login;
}

public function getLogin()
{
return $this->login;
}
}
YAML File:-

crv.event.subscriber.fun_event_subscriber:
class: AppBundle\EventListener\EventListener
tags:
- { name: kernel.event_listener, event: login.successfull, method: onLoginAction }

Default Contoller :- (When i will redirect to home page)

public function loginAction(Request $request)
{
$login = new Login();
$event = new LoginEvent($login);
$dispatcher = new EventDispatcher();
$dispatcher->dispatch(LoginEvent::LOGIN, $event);
$dispatcher->addListener('login.action', array($listener, 'onLoginAction'));

return $this->render('base.html.twig');
}

My ListnerClass :-

namespace AppBundle\EventListener;

class EventListener
{
public function onLoginAction(LoginEvent $event)
{
dump($event);
}
}

Can you pleae figure out where i'm making mistake.

-1 Reply
Default user avatar

Error i got after executing the code:-
Attempted to load class "Login" from namespace "AppBundle\Controller".
Did you forget a "use" statement for another namespace?

Reply

Hey Vivek Sharma

If you are using Symfony 3.3+ you can inject your dependencies as arguments in a controller's method, just as Ryan did:`
public function someAction(EventDispatcherInterface $dispatcher)`

So, you don't have to create a dispatcher object. What you have to do in your loginAction() is to inject the dispatcher, and dispatch your custom event only if the login was successful. Probably you will only need to pass the user object to your event, so you can print his name/nickname

I hope it helps you :)

Reply
Default user avatar
Default user avatar James D | posted 5 years ago

Hi folks - I seem to be falling at the first hurdle here. When I run php app/console doctrine:schema:create I get the following error:

[Doctrine\Common\Annotations\AnnotationException]
You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.

I've read around and it seems like the issue might be with PHP7, which is what I've got installed locally - but I couldn't find a satisfactory solution. So I put everything inside a Vagrant box with PHP5.6, and the doctrine commands work just fine - but I can't see how to load the site itself. I've made the start dir from your code tar into the Vagrant docroot, but when I go to the corresponding URL I just get a forbidden. I can't see an index file or anything like that in your code.

Am I missing something?

Thanks

Reply

Yo James D!

Let's see if I can help!

You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.

Basically, you need to tweak some php.ini settings so that annotations load. I'm a bit surprised to see this, as I believe the default settings in PHP 7 should be ok... but no problem. First, find your php.ini file by running:


php --ini

Now that you know where your php.ini file lives, open it and add/modify:


opcache.load_comments=1

Then, restart your web server if you're using one. If you're using bin/console server:run command, just stop and restart that.

but I can't see how to load the site itself

Ah, so here's the trick! First, because we're going to use the built-in PHP web server (just for development), you don't actually need to put your code in the Vagrant docroot - but that certainly doesn't hurt anything! When you start the built-in web server, you need to bind to all IP addresses (if this is the first time you've done this, I admit, it looks a bit technical:


php bin/console server:run 0.0.0.0

Now, find what the IP address is to your Vagrant box - e.g 192.168.33.10. Then, put that in your browser: http://192.168.33.10:8000.

That should load the site! In a nut-shell, you first start the built-in PHP web server inside Vagrant. But to access it on your host machine, you won't use "localhost" like normal - as Vagrant is like having a remote machine. Instead, you'll use the IP address to your Vagrant box. The reason we need to do the 0.0.0.0 thing is that - by default - the PHP web server only accepts requests coming from the local (in this case Vagrant) machine. By saying 0.0.0.0, it says "Respond to requests that come from ANY IP address". This allows us to access from our outside (host) machine.

And if you want to use Nginx or Apache, you can! But Symfony's "document root" is actually the "web" directory. The easiest way to make this work is to do some symbolic link magic. For example, suppose /var/www is the document root in Vagrant. I would do this:


# start in your home directory... or really ANY directory - it doesn't matter
cd ~
# put the start code here - in a "symfony_project" directory
# ... commands putting the start code here

# delete the document root
cd /var
sudo rm -rf www

# replace the document root with a symbolic link to your project's web directory
sudo ln -s ~/symfony_project/web www

That's it! Suddenly, your document root - /var/www - points to the "web" directory of your project.

Let me know if any of this helped! Hopefully we can get your past the first hurdle - setup problems are no fun ;).

Cheers!

1 Reply
Default user avatar

Amazing - thanks for your help, Ryan.

I ended up figuring it out like this (subtly different to your answer, so I thought I'd post here in case it helps anyone else):

The opcache settings are actually in /usr/local/php5/php.d/20-extension-opcache.ini, rather than in the core php.ini file (I found it by using php --ini, as you suggested).

By default opcache is switched off, so you can't use opcache.load_comments. In order to switch it on, I commented out the path to opache.so (which doesn't seem to be there), and I enabled opcache and the comments extension, like this:

;zend_extension=/usr/local/php5/lib/php/extensions/no-debug-non-zts-20151012/opcache.so
[opcache]
opcache.enable=1
opcache.load_comments=1

Then I reloaded Apache and Bob's your uncle.

FYI I'm using PHP7 and Apache 2 on a Mac running El Capitan.

Thanks for getting back to me. You guys are always really really helpful on the forums - it's one of the reasons I think this site is totally worth paying for.

2 Reply

You're awesome for sharing your fix details :). Cheers!

Reply
Default user avatar

The url to the dino images are not working for example http://0.tqn.com/y/dinosaur... does not exist. I think this needs to be updated

Reply

Hey Hank

Look like the fixtures links are broken, thanks for informing us. We will fix it as soon as possible.

Cheers!

Reply
Default user avatar

When I use dump($event) I get the error:

ContextErrorException in VarCloner.php line 144:
Notice: A non well formed numeric value encountered
in VarCloner.php line 144
at ErrorHandler->handleError('8', 'A non well formed numeric value encountered', '/var/www/aqua_note/vendor/symfony/symfony/src/Symfony/Component/VarDumper/Cloner/VarCloner.php', '144', array('var' => object(GetResponseEvent), 'useExt' => false, 'i' => '0', 'len' => '1', 'pos' => '0', 'refs' => '0', 'queue' => array(array(object(GetResponseEvent))), 'arrayRefs' => array(), 'hardRefs' => array(), 'objRefs' => array(), 'resRefs' => array(), 'values' => array(), 'maxItems' => '2500', 'maxString' => '-1', 'cookie' => object(stdClass), 'gid' => '33780576259ce40784a5ff7.42781480', 'a' => null, 'stub' => null, 'zval' => array('type' => 'object', 'zval_isref' => false, 'zval_hash' => null, 'array_count' => null, 'object_class' => null, 'object_handle' => null, 'resource_type' => null), 'hashMask' => '65 (0) refcount(2){ } ', 'hashOffset' => '8', 'indexed' => true, 'j' => '0', 'step' => array(object(stdClass)), 'v' => object(GetResponseEvent), 'k' => '0'))

What could be wrong?

Reply

Hey Hank

This is a Symfony (old version) incompatibility with PHP 7, you need to upgrade your Symfony's version to at least 2.7 or higher

Cheers!

Reply
Default user avatar
Default user avatar Hank | MolloKhan | posted 5 years ago | edited

Thanks @Diego Aguiar:disqus I'll do that!

Reply
Default user avatar
Default user avatar Shairyar Baig | posted 5 years ago | edited

Hello,

I was wondering if I can get some help on this as I am trying to understand EventSubscriber in forms. I have a form and in that i have ->addEventSubscriber(new SkillsSubscriber()) but inside the the subscriber class SkillsSubscriber() none of my events are firing


namespace AppBundle\Form\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class SkillsSubscriber implements EventSubscriberInterface
{

	public static function getSubscribedEvents()
	{

		return array(
			FormEvents::PRE_SUBMIT => 'onPreSubmit',
		);
	}

	public function onPreSubmit(FormEvent $event)
	{

		$user = $event->getData();
		$form = $event->getForm();
		dump($user, $form);
		die;

	}
}

All I am trying to do is when the form is submitted, I want to get hold of the data and change few things before its inserted in the database. What am i missing here? I am not sure if I should be using subscriber or listener here.

Reply

Hey Shairyar!

I believe you forgot to register your listener as a service (You can watch how in the video, around 4:30). Subscribers and listeners are almost the same, the only thing that changes is the way that they are hooked up into the system.

FYI: there are "FormEvents" that you can attach directly to your "FormTypes", I think they are perfect for your current task, you can read more about it here: http://symfony.com/doc/curr...

Have a nice day!

Reply
Default user avatar
Default user avatar Shairyar Baig | MolloKhan | posted 5 years ago

Thanks for getting back. It was a stupid mistake at my end the subscriber was implemented just fine I was looking at the wrong route :) everything is working as expected. thanks

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.x-dev", // 2.6.x-dev
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.5.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.7
        "symfony/monolog-bundle": "~2.4", // v2.6.1
        "sensio/distribution-bundle": "~3.0", // v3.0.9
        "sensio/framework-extra-bundle": "~3.0", // v3.0.3
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "~0.2" // 0.2
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.4.0
    }
}
userVoice