2016-02-05 18 views
8

Tôi đã xem this other question. Tôi đang tìm kiếm một cách để làm những gì OP của câu hỏi đó muốn là tốt, và đó là continue processing php after sending http response, nhưng trong Symfony2.Mã chạy Symfony sau khi phản hồi đã được gửi

Tôi đã triển khai sự kiện kích hoạt sau mỗi lần kết thúc hạt nhân. Cho đến nay rất tốt, nhưng những gì tôi muốn là cho nó để bắn sau khi chấm dứt CERTAIN, trong hành động điều khiển cụ thể, ví dụ sau khi một hình thức đã được gửi đi, không phải mỗi lần duy nhất ở mọi yêu cầu. Đó là vì tôi muốn thực hiện một số tác vụ nặng vào những thời điểm nhất định và không muốn người dùng cuối chờ trang tải.

Bất kỳ ý tưởng nào tôi có thể làm điều đó?

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

Cho đến bây giờ tôi đã đăng ký sự kiện vào hạt nhân. Tuy nhiên, rõ ràng điều này sẽ kích hoạt sự kiện theo từng yêu cầu. Tôi đã làm cho nó tương tự như của Swiftmailer EmailSenderListener

Thật lạ lùng khi hạt nhân phải lắng nghe mỗi lần cho sự kiện này ngay cả khi nó không được kích hoạt. Tôi thà bị sa thải chỉ khi cần thiết, nhưng không chắc chắn làm thế nào để làm điều đó.

Trả lời

7

Trong cuộc gọi lại onTerminate, bạn nhận được một phiên bản PostResponseEvent làm thông số đầu tiên. Bạn có thể nhận Yêu cầu cũng như Phản hồi từ đối tượng đó. Sau đó, bạn sẽ có thể quyết định xem bạn có muốn chạy mã chấm dứt thực tế không.

Ngoài ra, bạn có thể lưu trữ dữ liệu tùy chỉnh trong túi thuộc tính của Yêu cầu. Xem liên kết này: Symfony and HTTP Fundamentals

Lớp yêu cầu cũng có thuộc tính công khai chứa dữ liệu đặc biệt liên quan đến cách ứng dụng hoạt động trong nội bộ. Đối với Symfony Framework, các thuộc tính chứa các giá trị được trả về bởi route phù hợp, như _controller, id (nếu bạn có một ký tự đại diện {id}), và thậm chí cả tên của route phù hợp (_route). Thuộc tính thuộc tính tồn tại hoàn toàn là nơi bạn có thể chuẩn bị và lưu trữ thông tin theo ngữ cảnh cụ thể về yêu cầu.

Mã của bạn có thể trông giống như thế này:

// ... 

class TestListener implements EventSubscriberInterface 
{ 
    // ... 

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

    // ... 
} 

Edit:

Sự kiện kernel.terminate được thiết kế để chạy sau khi phản ứng được gửi đi. Nhưng tài liệu của symfony nói như sau (lấy từ here):

Bên trong, HttpKernel sử dụng hàm fastcgi_finish_request PHP. Điều này có nghĩa là tại thời điểm này, chỉ có API máy chủ FPM PHP mới có thể gửi phản hồi cho máy khách trong khi quá trình PHP của máy chủ vẫn thực hiện một số tác vụ. Với tất cả các API máy chủ khác, người nghe đến kernel.terminate vẫn được thực hiện, nhưng phản hồi không được gửi đến máy khách cho đến khi tất cả chúng được hoàn tất.

Chỉnh sửa 2:

Để sử dụng các giải pháp từ here, bạn có thể trực tiếp chỉnh sửa web/app.php tập tin để thêm nó ở đó (nhưng đây là một số loại "hack cốt lõi" imo , mặc dù nó sẽ dễ sử dụng hơn những điều sau đây).Hoặc bạn có thể làm như sau:

  1. Thêm một người nghe vào sự kiện kernel.request với mức độ ưu tiên cao và bắt đầu bộ đệm đầu ra (ob_start).
  2. Thêm người nghe vào kernel.response và thêm các giá trị tiêu đề vào phản hồi.
  3. Thêm một trình nghe khác có mức ưu tiên cao nhất vào kernel.terminate và thực hiện xả (ob_flush, flush).
  4. Chạy mã của bạn trong một người biết lắng nghe riêng biệt với ưu tiên thấp hơn để kernel.terminate

Tôi không thử nó, nhưng nó thực sự nên làm việc.

+0

Và nơi nào tôi thực hiện của tôi nhiệm vụ nặng nề? Tôi cần phải bắt đầu nhiệm vụ và chạy nó ngay cả sau khi phản hồi được gửi đi. Câu trả lời không được chờ cho nhiệm vụ hoàn thành. Tôi có phải 'làm công cụ' như [ở đây] (http://stackoverflow.com/a/15273676/2077972) không? Hoặc là phản hồi được gửi sau khi bất kỳ điều gì xảy ra trong hàm onTerminate() theo mặc định? –

+0

Sự kiện chấm dứt được thiết kế để chạy sau khi phản hồi được gửi đi. Vì vậy, khi "onTerminate" được gọi là phản hồi nên đã được ra cho người dùng. –

+0

Rõ ràng trong thực tế, điều này không phải luôn luôn như vậy. Kiểm tra câu trả lời đã chỉnh sửa của tôi. –

0

tôi đã sử dụng những câu trả lời để viết một lớp Phản hồi có chức năng này: https://stackoverflow.com/a/28738208/1153227

thực hiện này sẽ làm việc trên Apache và không chỉ PHP FPM. Tuy nhiên, để thực hiện công việc này, chúng ta phải ngăn Apache sử dụng gzip (bằng cách sử dụng Mã hóa nội dung không hợp lệ) để có một lớp Response tùy chỉnh để xác định chính xác khi nào có phản hồi sớm quan trọng hơn nén.

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

Bạn cũng cần phải thêm một phương pháp để AppKernel.php của bạn để làm cho công việc này (đừng quên thêm một tuyên bố sử dụng cho lớp EarlyResponse của bạn)

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(); 
} 
Các vấn đề liên quan