yano3nora
11/14/2017 - 5:59 AM

[cakephp: API Component & Controller] Cake modules for ajax request and Server-Sent Event. #cakephp

[cakephp: API Component & Controller] Cake modules for ajax request and Server-Sent Event. #cakephp

<?php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;

/**
 * Api Component.
 * @access protected
 * @category Ajax API
 * @package  Cake\Controller\Component
 * @author   hyano@ampware.jp
 */
class ApiComponent extends Component
{


  /** 
   * Properties
   * Set in initialize method.
   */ 
  private   $Controller;
  private   $code    = 200;
  public    $message = 'API is working.';
  public    $debug = 'Printing on debug mode.';


  /**
   * Initialize method
   * @param array $config
   * @return void
   */
  public function initialize(array $config) {
    parent::initialize($config);
    $this->Controller = $this->_registry->getController();
  }
  
  
  /**
   * Startup method
   * - Set controller properties (view / response).
   * - Detection bad request without authority.
   * @param void
   * @return Cake\Http\Response (json) (on failure)
   */
  public function startup() {
    $this->Controller->viewBuilder()->autoLayout(false);
    $this->Controller->autoRender = false;
    $this->response = $this->response->withCharset(ENCODING);
    if ($this->request->action == 'fetch') {
      // Server-Sent Events API.
      header("Content-Type: text/event-stream\n");
      header("Cache-Control: no-cache\n");
    } else {
      // Ajax API.
      $this->response = $this->response->withType('application/json');
      if (!$this->request->is('ajax') || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest') {
        return $this->printError(400, 'Failed to API request by Ajax. Please verify your request header.');
      }
    }
  }


  /**
   * Print JSON Response
   * Set & Print response as JSON
   * @param array|null  $results
   * @param string|null $message
   * @param int|null    $code
   * @return Cake\Http\Response (json)
   */
  public function printJsonResponse(
    array  $results = null,
    string $message = null,
    int    $code    = null
  ) { /* Print response hash as JSON */
    $status  = !empty($results);
    $code    = $code    ?? $this->code;
    $message = $message ?? $this->message;
    $results = $results ?? [];
    $response = [
      'status'  => $status,
      'code'    => $code,
      'message' => $message,
      'results' => $results,
    ];
    if (DEBUG_MODE) $response += ['debug' => $this->debug];
    $this->response = $this->response->withStatus($code);
    $this->response = $this->response->withStringBody(json_encode(compact('response')));
    return $this->response;
  }


  /** 
   * Print error
   * Print response as JSON for illegal access.
   * @param int $code
   * @param string $message
   */
  public function printError(int $code=500, string $message=null) {
    return $this->printJsonResponse(null, $message, $code);
  }
  

  /**
   * Print event method for Server-Sent Events.
   * @param mixed   $data
   * @param ?string $event
   * @param ?int    $sleep
   * @param ?int    $retry
   * @see https://gist.github.com/a08b20aea47f509d8947e6bb63dcb9e3
   */
  public function printFetchEvent($data, string $event='message', int $sleep=1, int $retry=0) {
    $print = 'event: '.$event.LF;
    if (is_array($data) || is_object($data)) {
      $print .= 'data: '.json_encode($data).LF;
    } else {
      $print .= 'data: '.$data.LF;
    }
    if ($retry) {
      $print .= 'retry: '.($retry * 1000).LF;  // Milliseconds.
    }
    $print .= LF;
    echo $print;
    ob_flush();
    flush();
    session_write_close();
    sleep($sleep);
  }


  /** 
   * Flush method for Server-Sent Events.
   * @param  int $sleep
   * @return void
   */
  public function printFetchFlush(int $sleep=1) {
    ob_flush();
    flush();
    session_write_close();
    sleep($sleep);
  }


}
<?php
// ApiController
namespace App\Controller\Api;
use App\Controller\AppController;
use Cake\Datasource\ConnectionManager; 
class ApiController extends AppController
{
  
  /**
   * Properties.
   */
  public $connection = null;  // Cake\Datasource\ConnectionInterface;

  /**
   * Initialize method
   * Add loading components.
   */
  public function initialize() {
    parent::initialize();
    $this->loadComponent('Api');
    $this->connection = ConnectionManager::get('default');
  }

  /**
   * Response status
   * @param void
   * @return Cake\Http\Response (json)
   */
  public function status() {
    return $this->Api->printJsonResponse([
      'debug_mode' => DEBUG_MODE,
      'request'    => $this->request->url,
      'authority'  => $this->Auth->user(),
    ]);
  }

}



// HogeController (extends ApisController)
namespace App\Controller\Api;
use App\Controller\Api\ApiController;
class QueuesController extends ApiController
{

  /** 
   * Pull for GET access.
   */
  public function pull() {
    if (!$this->Auth->user()) $this->Api->printError(403);
    if ($this->request->is('get')) { 
      $queues = $this->Queues->find('list');
      $this->Api->message = $queues->count().' queues found.'
      return $this->Api->printJsonResponse($queues->toArray());
    }
  }

  /**
   * Fetch for Server Sent Event.
   */
  public function fetch() {
    $started = new \DateTimeImmutable();
    while (strtotime('now') <= $started->modify('+'.LOOP_TIME_LIMIT.' seconds')->format('U')) {
      $queues = $this->Queues->find()->where(['is_fetched' => false]);
      if (!$queues->count()) {
        $this->Api->printFetchEvent('Polling is alive.', 'ping', WAIT);
        continue;
      }
      try {
        $this->connection->begin();
        // Flagging to queues as fetched, then send event.
        foreach ($queues as $queue) {
          $tmpQueue = $this->Queues->patchEntity($queue, ['is_fetched' => true]);
          if (!$this->Queues->save($tmpQueue)) throw new \Exception('Failed to save fetched records.');
        }
        $this->connection->commit();
        $this->Api->printFetchEvent($queues, 'push', WAIT);
      } catch (\Exception $e) {
        $this->connection->rollback();
        $this->Api->printFetchEvent($e->getMessage(), 'error', WAIT);
      }
    }
    exit;
  }

}