我看了另一个问题。我正在寻找一种方法来做这个问题的OP想要的事情,那就是在发送http响应后继续处理php,但在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 的实例作为第一个参数。您可以从该对象获取请求和响应。然后,您应该能够决定是否要运行实际的终止代码。
您还可以将自定义数据存储在请求的属性包中。请参阅此链接:Symfony 和 HTTP Fundamentals
Request 类还具有一个公共属性属性,该属性保存与应用程序内部工作方式相关的特殊数据。对于Symfony框架,属性保存匹配路由返回的值,如_controller,id(如果你有{id}通配符(,甚至是匹配路由的名称(_route(。属性完全是为了准备和存储有关请求的上下文特定信息而存在的。
您的代码可能如下所示:
// ...
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文档说的是以下内容(取自这里(:
在内部,HttpKernel使用fastcgi_finish_request PHP函数。这意味着目前,只有 PHP FPM 服务器 API 能够向客户端发送响应,而服务器的 PHP 进程仍在执行某些任务。对于所有其他服务器 API,内核终止的侦听器仍会执行,但在它们全部完成之前,响应不会发送到客户端。
编辑 2:
要从这里使用解决方案,您可以直接编辑 web/app.php 文件以将其添加到其中(但这是某种"黑客核心"imo,即使它比以下内容更容易使用(。或者你可以这样做:
- 将侦听器添加到具有高优先级的 kernel.request 事件并启动输出缓冲 (ob_start(。
- 将侦听器添加到 kernel.response,并将标头值添加到响应中。
- 将另一个具有最高优先级的侦听器添加到 kernel.terminate 并执行刷新(ob_flush,刷新(。
- 在单独的侦听器中运行代码,内核优先级较低。终止
我没有尝试过,但它实际上应该有效。
为了解决我的一些用例的这个问题,我只是简单地创建symfony命令来完成繁重的任务,并通过exec((调用它们以使它们在单独的进程中运行。
我使用这些答案编写了一个具有此功能的响应类:https://stackoverflow.com/a/28738208/1153227
这个实现将适用于Apache,而不仅仅是PHP FPM。但是,要完成这项工作,我们必须防止 Apache 使用 gzip(通过使用无效的内容编码(,因此拥有一个自定义 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 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();
}