2016-02-05 30 views
8

Przyjrzałem się this other question. Szukam sposobu, aby zrobić to, co OP tego pytania również chce, a to jest do continue processing php after sending http response, ale w Symfony2.Symfony uruchamiają kod po wysłaniu odpowiedzi

Zaimplementowałem zdarzenie uruchamiane po każdym zakończeniu jądra. Do tej pory tak dobrze, ale to, czego chcę, to to, żeby wystrzelił po PEWNYCH przerwach, w konkretnych działaniach kontrolera, na przykład po wysłaniu formularza, nie za każdym razem na każde żądanie. Dzieje się tak dlatego, że chcę wykonywać pewne ciężkie zadania w określonym czasie i nie chcę, aby użytkownik końcowy oczekiwał na załadowanie strony.

Każdy pomysł, jak mogę to zrobić?

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

Jak dotąd subskrybowanych zdarzenie do kernel.terminate jednego, ale oczywiście ta wystrzeliwuje go na każde żądanie. Zrobiłem to, co podobne do Swiftmailer'a: EmailSenderListener To trochę dziwne, że jądro musi nasłuchiwać za każdym razem, nawet jeśli nie zostanie wyzwolone. Wolałbym, żeby był wyrzucany tylko w razie potrzeby, ale nie wiem, jak to zrobić.

Odpowiedz

7

W wywołaniu zwrotnym onTerminate jako pierwszy parametr otrzymuje się instancję PostResponseEvent. Możesz otrzymać Żądanie, a także Odpowiedź od tego obiektu. Następnie powinieneś być w stanie zdecydować, czy chcesz uruchomić aktualny kod zakończenia.

Można również przechowywać niestandardowe dane w torbie atrybutów żądania. Zobacz ten link: Symfony and HTTP Fundamentals

Klasa żądania ma również właściwość atrybutów publicznych, która zawiera specjalne dane dotyczące sposobu działania aplikacji. W przypadku Symfony Framework, atrybuty przechowuje wartości zwracane przez dopasowaną trasę, takie jak _controller, id (jeśli masz {id} wieloznaczną), a nawet nazwę dopasowanej trasy (_route). Właściwość attributes istnieje wyłącznie w miejscu, w którym można przygotować i przechowywać informacje dotyczące danego kontekstu dotyczące danego żądania.

Twój kod mógłby wyglądać tak:

// ... 

class TestListener implements EventSubscriberInterface 
{ 
    // ... 

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

    // ... 
} 

Edit:

kernel.terminate wydarzenie jest przeznaczony do uruchamiania po odpowiedź zostanie wysłana. Ale dokumentacja Symfony mówi następująco (zaczerpnięte z here):

Wewnętrznie HttpKernel wykorzystuje fastcgi_finish_request funkcji PHP. Oznacza to, że w tej chwili tylko interfejs API serwera PHP FPM jest w stanie wysłać odpowiedź do klienta, podczas gdy proces PHP serwera nadal wykonuje pewne zadania. W przypadku wszystkich innych interfejsów API serwera detektory kernel.terminate są nadal wykonywane, ale odpowiedź nie jest wysyłana do klienta, dopóki wszystkie nie zostaną zakończone.

Edit 2:

Aby korzystać z rozwiązania z here, można bezpośrednio edytować w internecie/app.php plik, aby go dodać tam (ale to jest jakiś rodzaj "hacking rdzeń" imo , chociaż byłoby to łatwiejsze w użyciu niż poniższe).Lub możesz to zrobić tak:

  1. Dodaj odbiorcę do zdarzenia kernel.request z wysokim priorytetem i uruchom buforowanie wyjściowe (ob_start).
  2. Dodaj detektora do kernel.response i dodaj wartości nagłówka do odpowiedzi.
  3. Dodaj kolejnego słuchacza o najwyższym priorytecie do jądra.terminuj i wykonaj płukanie (ob_flush, flush).
  4. Uruchom swój kod w oddzielnym słuchacza o niższym priorytecie do kernel.terminate

nie próbowałem, ale powinno to rzeczywiście działa.

+0

i gdzie mogę wykonać mój ciężkie zadanie? Muszę uruchomić zadanie i uruchomić go nawet po wysłaniu odpowiedzi. Odpowiedź nie może czekać na zakończenie zadania. Czy muszę "robić rzeczy" jak [tutaj] (http://stackoverflow.com/a/15273676/2077972)? Lub czy odpowiedź jest wysyłana po tym, co dzieje się w funkcji onTerminate() domyślnie? –

+0

Zdarzenie zakończenia jest przeznaczone do uruchamiania po wysłaniu odpowiedzi. Więc kiedy wywoływana jest funkcja "onTerminate", odpowiedź powinna już być dostępna dla użytkownika. –

+0

Podobno w praktyce nie zawsze tak jest. Sprawdź moją zredagowaną odpowiedź. –

0

Kiedyś te odpowiedzi napisać klasę Response, który ma tę funkcjonalność: https://stackoverflow.com/a/28738208/1153227

Ta implementacja będzie działać na Apache i PHP nie tylko FPM. Jednak, aby to działało, musimy uniemożliwić Apacheowi używanie gzip (przy użyciu nieprawidłowego kodowania treści), więc ma sens posiadanie niestandardowej klasy Response, aby dokładnie określić, kiedy wczesna odpowiedź jest ważniejsza niż kompresja.

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

Trzeba także dodać metodę do AppKernel.php dokonania tej pracy (nie zapomnij dodać oświadczenie użyć jako klasy EarlyResponse)

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