发送响应后的 Symfony 运行 代码

Symfony run code after response was sent

我看了看 this other question. I am looking for a way to do what the OP of that question wants as well, and that is to ,但是在 Symfony2 中。

我实现了一个在每次内核终止后触发的事件。到目前为止一切顺利,但我想要的是在某些特定的控制器操作中,例如在发送表单之后,而不是在每次请求时都在某些终止后触发。那是因为我想在某些时候做一些繁重的任务,不希望最终用户等待页面加载。

知道我该怎么做吗?

<?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;
    }
}

到目前为止,我已将事件订阅到 kernel.terminate 事件,但显然这会在每次请求时触发它。我使它类似于 Swiftmailer 的 EmailSenderListener

感觉有点奇怪,内核必须每次都监听这个事件,即使它没有被触发。我宁愿只在需要时触发它,但不确定该怎么做。

在 onTerminate 回调中,您将获得 PostResponseEvent 的实例作为第一个参数。您可以从 object 获得请求和响应。 然后你应该能够决定是否要 运行 实际的终止代码。

您还可以将自定义数据存储在请求的属性包中。看到这个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.

您的代码可能如下所示:

// ...

class TestListener implements EventSubscriberInterface
{
    // ...

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

    // ...
}

编辑:

kernel.terminate事件设计为运行响应发送后。但是 symfony 文档是这样说的(摘自 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.

编辑 2:

要使用 here 中的解决方案,您可以直接编辑 web/app.php 文件以将其添加到那里(但这是某种 "hacking core" imo,即使它比下面的更容易使用)。或者你可以这样做:

  1. 向具有高优先级的 kernel.request 事件添加侦听器并开始输出缓冲 (ob_start)。
  2. 向 kernel.response 添加侦听器并将 header 值添加到响应。
  3. 将另一个具有最高优先级的侦听器添加到 kernel.terminate 并进行刷新(ob_flush,刷新)。
  4. 运行 您的代码在单独的侦听器中,优先级低于 kernel.terminate

我没有尝试过,但它应该确实有效。

我使用这些答案编写了具有以下功能的响应 class:

此实现将在 Apache 上运行,而不仅仅是 PHP FPM。然而,为了使这项工作有效,我们必须阻止 Apache 使用 gzip(通过使用无效的内容编码),因此使用自定义 Response class 来准确指定何时早期响应比压缩更重要是有意义的。

use Symfony\Component\HttpFoundation\Response;

class EarlyResponse extends Response
{
    // Functionality adapted from this answer: 

    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;
    }
}

您还需要向您的 AppKernel.php 添加一个方法来完成这项工作(不要忘记为您的 EarlyResponse 添加一个 use 语句 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();
}

为了解决我的一些用例的这个问题,我简单地创建了 symfony 命令来完成繁重的任务,并通过 exec() 调用它们,使它们 运行 在一个单独的进程中。