Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony run code after response was sent

I took a look at this other question. I am looking for a way to do what the OP of that question wants as well, and that is to continue processing php after sending http response, but in Symfony2.

I implemented an event that fires after every kernel termination. So far so good, but what I want is for it to fire after CERTAIN terminations, in specific controller actions, for instance after a form was sent, not every single time at every request. That is because I want to do some heavy tasks at certain times and don't want the end user to wait for the page to load.

Any idea how can I do that?

<?php


namespace MedAppBundle\Event;

use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use JMS\DiExtraBundle\Annotation\Inject;
/**
 * Class MedicListener
 * @package MedAppBundle\EventListener
 * @Service("medapp_test.listener")
 * @Tag(name="kernel.event_subscriber")
 */
class TestListener implements EventSubscriberInterface
{
    private $container;

    private $logger;

    /**
     * Constructor.
     *
     * @param ContainerInterface $container A ContainerInterface instance
     * @param LoggerInterface $logger A LoggerInterface instance
     * @InjectParams({
     *     "container" = @Inject("service_container"),
     *     "logger" = @Inject("logger")
     * })
     */
    public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
    {
        $this->container = $container;
        $this->logger = $logger;
    }

    public function onTerminate()
    {
      $this->logger->notice('fired');
    }

    public static function getSubscribedEvents()
    {
        $listeners = array(KernelEvents::TERMINATE => 'onTerminate');

        if (class_exists('Symfony\Component\Console\ConsoleEvents')) {
            $listeners[ConsoleEvents::TERMINATE] = 'onTerminate';
        }

        return $listeners;
    }
}

So far I've subscribed the event to the kernel.terminate one, but obviously this fires it at each request. I made it similar to Swiftmailer's EmailSenderListener

It feels kind of strange that the kernel must listen each time for this event even when it's not triggered. I'd rather have it fired only when needed, but not sure how to do that.

like image 421
George Irimiciuc Avatar asked Feb 05 '16 08:02

George Irimiciuc


People also ask

How to retrieve the input from a command in Symfony controller?

From the execution of a command you can decide if you retrieve the input or not: With output. To execute a command within a controller, use the following code: The previous controller will return as response "My Third Symfony command ============ First line value : Hello Second line value : World" in the browser.

What happens when a message is retried in Symfony?

All of this is configurable for each transport: Symfony triggers a WorkerMessageRetriedEvent when a message is retried so you can run your own logic. Sometimes handling a message might fail in a way that you know is permanent and should not be retried. If you throw UnrecoverableMessageHandlingException , the message will not be retried.

How do I send a message in the background using Symfony?

The Symfony CLI can manage such background commands or workers by using the daemon flag ( -d) on the run command. Run the message consumer again, but send it in the background: $ symfony run -d --watch=config,src,templates,vendor symfony console messenger:consume async -vv

Why does my Symfony transport consume messages from async_priority_low?

If there are none, then it will consume messages from async_priority_low. Some transports (notably AMQP) have the concept of exchanges and queues. A Symfony transport is always bound to an exchange. By default, the worker consumes from all queues attached to the exchange of the specified transport.


1 Answers

In the onTerminate callback you get an instance of PostResponseEvent as first parameter. You can get the Request as well as the Response from that object. Then you should be able to decide if you want to run the actual termination code.

Also you can store custom data in the attributes bag of the Request. See this link: Symfony and HTTP Fundamentals

The Request class also has a public attributes property, which holds special data related to how the application works internally. For the Symfony Framework, the attributes holds the values returned by the matched route, like _controller, id (if you have an {id} wildcard), and even the name of the matched route (_route). The attributes property exists entirely to be a place where you can prepare and store context-specific information about the request.

Your code could look something like this:

// ...

class TestListener implements EventSubscriberInterface
{
    // ...

    public function onTerminate(PostResponseEvent $event)
    {
        $request = $event->getRequest();
        if ($request->attributes->get('_route') == 'some_route_name') {
            // do stuff
        }
    }

    // ...
}

Edit:

The kernel.terminate event is designed to run after the response is sent. But the symfony documentation is saying the following (taken from here):

Internally, the HttpKernel makes use of the fastcgi_finish_request PHP function. This means that at the moment, only the PHP FPM server API is able to send a response to the client while the server's PHP process still performs some tasks. With all other server APIs, listeners to kernel.terminate are still executed, but the response is not sent to the client until they are all completed.

Edit 2:

To use the solution from here, you could either directly edit the web/app.php file to add it there (but this is some kind of "hacking core" imo, even though it would be easier to use than the following). Or you could do it like this:

  1. Add a listener to kernel.request event with a high priority and start output buffering (ob_start).
  2. Add a listener to kernel.response and add the header values to the response.
  3. Add another listener with highest priority to kernel.terminate and do the flushing (ob_flush, flush).
  4. Run your code in a separate listener with lower priority to kernel.terminate

I did not try it, but it should actually work.

like image 71
Tobias Xy Avatar answered Oct 03 '22 18:10

Tobias Xy