Symfony在发送响应后运行代码

时间:2016-02-05 08:32:51

标签: php symfony httpresponse

我看了this other question。我正在寻找一种方法来做那个问题的OP也想要的,那就是continue processing php after sending http response,但是在Symfony2中。

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

知道我该怎么做?

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

类似

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

3 个答案:

答案 0 :(得分:8)

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

您还可以将自定义数据存储在请求的属性包中。请看这个链接:Symfony and HTTP Fundamentals

  

Request类还有一个公共属性属性,它包含与应用程序内部工作方式相关的特殊数据。对于Symfony框架,属性保存匹配路由返回的值,如_controller,id(如果您有{id}通配符),甚至是匹配路由的名称(_route)。 attributes属性完全存在于您可以准备和存储有关请求的特定于上下文的信息的位置。

您的代码可能如下所示:

// ...

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):

  

在内部,HttpKernel使用fastcgi_finish_request PHP函数。这意味着目前只有PHP FPM服务器API能够向客户端发送响应,而服务器的PHP进程仍然执行某些任务。对于所有其他服务器API,仍然会执行kernel.terminate的侦听器,但在完成所有响应之前,响应不会发送到客户端。

编辑2:

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

  1. 向具有高优先级的kernel.request事件添加侦听器并启动输出缓冲(ob_start)。
  2. 向kernel.response添加一个侦听器,并将标头值添加到响应中。
  3. 将另一个具有最高优先级的侦听器添加到kernel.terminate并执行刷新(ob_flush,flush)。
  4. 在内核中使用较低优先级的侦听器运行代码
  5. 我没试过,但它确实应该有用。

答案 1 :(得分:1)

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

答案 2 :(得分:0)

我使用这些答案编写了一个具有此功能的Response类: https://stackoverflow.com/a/28738208/1153227

此实现适用于Apache而不仅仅是PHP FPM。但是,为了完成这项工作,我们必须阻止Apache使用gzip(通过使用无效的Content-Encoding),因此有一个自定义的Response类来准确指定何时早期响应比压缩更重要。

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

您还需要在AppKernel.php中添加一个方法来完成这项工作(不要忘记为EarlyResponse类添加一个use语句)

public function terminate(Request $request, Response $response)
{

    ob_start();
    //Run this stuff before the terminate events
    if ($response instanceof \IFI2\BaseBundle\Response\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();
}