Symfony run code after response was sent Symfony run code after response was sent php php

Symfony run code after response was sent


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.


To solve this issue for some of my use cases I simply create symfony commands to do the heavy tasks, and call them via exec() to make them run in a separate process.


I used these answers to write a Response class that has this functionality:https://stackoverflow.com/a/28738208/1153227

This implementation will work on Apache and not just PHP FPM. However, to make this work we must prevent Apache from using gzip (by using an invalid Content-Encoding) so it makes sense to have a custom Response class to specify exactly when having an early response is more important than compression.

use Symfony\Component\HttpFoundation\Response;class EarlyResponse extends Response{    // Functionality adapted from this answer: https://stackoverflow.com/a/7120170/1153227    protected $callback = null;    /**     * Constructor.     *     * @param mixed $content The response content, see setContent()     * @param int   $status  The response status code     * @param array $headers An array of response headers     *     * @throws \InvalidArgumentException When the HTTP status code is not valid     */    public function __construct($content = '', $status = 200, $headers = array(), $callback = null)    {        if (null !== $callback) {            $this->setTerminateCallback($callback);        }        parent::__construct($content, $status, $headers);    }    /**     * Sets the PHP callback associated with this Response.     * It will be called after the terminate events fire and thus after we've sent our response and closed the connection     *     * @param callable $callback A valid PHP callback     *     * @throws \LogicException     */    public function setTerminateCallback($callback)    {        //Copied From Symfony\Component\HttpFoundation\StreamedResponse        if (!is_callable($callback)) {            throw new \LogicException('The Response callback must be a valid PHP callable.');        }        $this->callback = $callback;    }    /**     * @return Current_Class_Name     */    public function send() {        if (function_exists('fastcgi_finish_request') || 'cli' === PHP_SAPI) { // we don't need the hack when using fast CGI            return parent::send();        }        ignore_user_abort(true);//prevent apache killing the process        if (!ob_get_level()) { // Check if an ob buffer exists already.            ob_start();//start the output buffer         }        $this->sendContent(); //Send the content to the buffer        static::closeOutputBuffers(1, true); //flush all but the last ob buffer level        $this->headers->set('Content-Length', ob_get_length()); // Set the content length using the last ob buffer level        $this->headers->set('Connection', 'close'); // Close the Connection        $this->headers->set('Content-Encoding', 'none');// This invalid header value will make Apache not delay sending the response while it is         // See: https://serverfault.com/questions/844526/apache-2-4-7-ignores-response-header-content-encoding-identity-instead-respect        $this->sendHeaders(); //Now that we have the headers, we can send them (which will avoid the ob buffers)        static::closeOutputBuffers(0, true); //flush the last ob buffer level        flush(); // After we flush the OB buffer to the normal buffer, we still need to send the normal buffer to output        session_write_close();//close session file on server side to avoid blocking other requests        return $this;    }    /**     * @return Current_Class_Name     */    public function callTerminateCallback() {        if ($this->callback) {            call_user_func($this->callback);        }        return $this;    }}

You also need to add a method to your AppKernel.php to make this work (don't forget to add a use statement for your EarlyResponse class)

public function terminate(Request $request, Response $response){    ob_start();    //Run this stuff before the terminate events    if ($response instanceof EarlyResponse) {        $response->callTerminateCallback();    }    //Trigger the terminate events    parent::terminate($request, $response);    //Optionally, we can output the beffer that will get cleaned to a file before discarding its contents    //file_put_contents('/tmp/process.log', ob_get_contents());    ob_end_clean();}