You are here

AjaxPageControllerBase.php in Forena Reports 8

File

src/Controller/AjaxPageControllerBase.php
View source
<?php

/**
 * Created by PhpStorm.
 * User: metzlerd
 * Date: 3/10/2017
 * Time: 10:35 AM
 */
namespace Drupal\forena\Controller;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\UpdateBuildIdCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\forena\Form\ReportModalForm;
use Drupal\forena\Forena;
use Drupal\forena\FrxPlugin\AjaxCommand\AjaxCommandInterface;
use Drupal\forena\Context\AppContext;
abstract class AjaxPageControllerBase extends ControllerBase implements AjaxControllerInterface {
  const LAYOUT = 'sample/dashboard';
  const DEFAULT_ACTION = 'view';
  const TOKEN_PARAMETER = 'c';
  const TOKEN_PREFIX = 'ajax';
  const MAX_STATE_AGE = 43260;

  /**
   * @var string
   *   Controller token for keeping track of state information between page
   *   loads of the controller.
   */
  protected $token;

  /**
   * @var string Name of forena report used for layout
   */
  public $layout;

  /**
   * @var array
   *   Current parameters for the report.
   */
  public $parms = [];

  /**
   * @var bool
   *   Indicates whether the current form being processed is a modal.
   */
  public $is_modal_form = FALSE;

  /**
   * @var bool
   *   Inidates whether a modal has already been added
   */
  public $modal_added = FALSE;

  /**
   * @var string
   */
  public $section = '';
  public $prior_section = '';

  /** @var FormStateInterface */
  public $form_state;

  /** @var object */
  protected $state;

  /**
   * @var AjaxPageControllerBase
   *   Singleton instance.
   */
  protected static $instance;

  /**
   * @var array
   *   Array of librarries to load with this controller.
   */
  public $libraries = [];

  /**
   * Application context to load.
   * @var static
   */
  protected $context;

  /**
   * @var array
   *   Drupal render array containing content.
   */
  protected $build = [];

  /**
   * Render array of modal content.
   * @var array
   */
  public $modal_content = [];

  /**
   * The JSMode passed to the the controller to determine how to deliver the
   * page. "ajax" implies an ajax page load "nojs" implies a normal page.
   * @var string
   */
  public $jsMode;

  /**
   * Ajax commands that are to be returned by the object.
   * @var AjaxCommandInterface[]
   */
  protected $commands = [];

  /**
   * The final command that updates the page.  Note this is generally either
   * a setURL ajax command or it is a CloseDialog command.
   * @var
   */
  public $endCommand = NULL;

  /**
   * @var bool Prevent the further processing of default action pages
   */
  public $prevent_action = FALSE;
  public $action;
  public $post_form_id = '';

  /**
   * Singleton factory method.
   * @return static
   */
  public static function service() {
    if (static::$instance === NULL) {
      static::$instance = new static();
    }
    return static::$instance;
  }

  /**
   * Indicates whether the current callback is an ajax call.
   * @return bool
   */
  public function isAjaxCall() {
    return $this->jsMode != 'nojs';
  }

  /**
   * AjaxPageControllerBase constructor.
   */
  public function __construct() {
    static::$instance = $this;
    if (isset($_GET)) {
      $this->parms = $_GET;
      if (isset($_REQUEST[static::TOKEN_PARAMETER])) {
        $this->token = $_REQUEST[static::TOKEN_PARAMETER];
        unset($this->parms[static::TOKEN_PARAMETER]);
      }
      unset($this->parms['_wrapper_format']);
      unset($this->parms['ajax_form']);
    }
    $this->context = AppContext::create();
    $this->layout = static::LAYOUT;
    $this->libraries[] = 'forena/forena';

    // Set the context for the forena applcation.
    $this
      ->setReportContext('app', $this->context);

    // Load the state if it hasn't been loaded.
    $this
      ->loadState();
  }

  /**
   * Sets the report context for a report.
   * @param $name
   * @param $value
   */
  public function setReportContext($name, &$value) {
    Forena::service()
      ->setDataContext($name, $value);
  }

  /**
   * Generate a token for form
   */
  protected function generateStateToken() {
    $this->token = static::TOKEN_PREFIX . '-' . bin2hex(openssl_random_pseudo_bytes(20));
  }

  /**
   * Return the state token.
   * @return string
   */
  public function getStateToken() {
    if (!$this->token) {
      $this
        ->generateStateToken();
    }
    return $this->token;
  }

  /**
   * @return Object|NULL
   */
  public function getState() {
    return $this->state;
  }

  /**
   * Loads the state from the token.
   */
  protected function loadState() {
    if (!$this->token) {
      $this
        ->generateStateToken();
    }
    else {
      $svc = \Drupal::keyValueExpirable(static::TOKEN_PREFIX);
      $data = $svc
        ->get($this->token);
      if ($data) {
        $this->state = unserialize($data);
      }
    }
  }

  /**
   * Save the state of the controller
   */
  public function saveState() {
    if ($this->state !== NULL) {
      $state = serialize($this->state);
      $svc = \Drupal::keyValueExpirable(static::TOKEN_PREFIX);
      $svc
        ->setWithExpire($this->token, $state, static::MAX_STATE_AGE);
    }
  }

  /**
   * Default page controller implementation.
   * This method is typically what you would reference in the routing.yml
   * file as the main menu callback for the controller.
   *
   * @param $action
   * @param string $js_mode
   * @return array|\Symfony\Component\HttpFoundation\RedirectResponse
   */
  public function page($action = '', $js_mode = 'nojs') {
    $this->jsMode = $js_mode;
    if (!empty($_GET['_wrapper_format'])) {
      $this->jsMode = $_GET['_wrapper_format'];
    }
    if (!$action && $this->jsMode == 'nojs') {
      $route_name = \Drupal::routeMatch()
        ->getRouteName();
      return $this
        ->redirect($route_name, [
        'action' => static::DEFAULT_ACTION,
      ]);
    }
    else {
      $build = $this
        ->response($action);
      return $build;
    }
  }

  /**
   * Return the response based on ajax pramters.
   * @param string $action
   *   The action used by the reout to generate a reponse.
   * @return array | null
   *   Drupal render array containing action.
   */
  public function response($action) {
    $this->action = $action;
    switch ($this->jsMode) {
      case 'nojs':
        $this
          ->initLayout();
        $this
          ->processFormRequest();
        if (!$this->prevent_action) {
          $this
            ->route($action);
        }
        foreach ($this->build as $section => $content) {
          if (is_array($content)) {
            $content = \Drupal::Service('renderer')
              ->render($content);
          }
          $this->context->{$section} = $content;
        }
        $content = Forena::service()
          ->report($this->layout);

        // Add the ajax librarries used by teh controller.
        if (!empty($content['#attached']['library'])) {
          $content['#attached']['library'] = array_merge($content['#attached']['library'], $this->libraries);
        }
        else {
          $content['#attached']['library'] = $this->libraries;
        }
        $response = $content;
        break;
      case 'drupal_modal':

        // This is the type sent when you use the "data-dialog-type" class.
        // Note that it is not the common way to display a modal.
        $this
          ->processFormRequest();
        if (!$this->prevent_action) {
          $this
            ->route($action);
        }
        $response = $this->build;
        break;
      default:

        // All other types are assumed to be some ajax variant.
        $this
          ->processFormRequest();
        if (!$this->prevent_action) {
          $this
            ->route($action);
        }
        $commands = $this
          ->getCommands();

        //print ajax_render($commands);
        $response = new AjaxResponse();
        foreach ($commands as $command) {
          $response
            ->addCommand($command);
        }
    }
    $this
      ->saveState();
    return $response;
  }

  /**
   * Process the post requests for an action.
   */
  public function processFormRequest() {
    if (!empty($_REQUEST['form_id'])) {
      $form_id = $_REQUEST['form_id'];
      $this
        ->processForm($form_id);
    }
  }

  /**
   * Processes the form based on a form_id
   * @param string $form_id
   */
  public function processForm($form_id) {
    switch ($form_id) {
      case ReportModalForm::FORM_ID:
        $this
          ->getModalForm('main', ReportModalForm::class, '');
    }
  }
  public function preventAction($prevent_action = TRUE) {
    $this->prevent_action = $prevent_action;
  }

  /**
   * @param string $section
   *   The location where the content should go.
   * @param string|array $content
   *   The content to replace.
   */
  public function render($section, $content) {
    if ($this->jsMode != 'nojs' && $this->jsMode != 'drupal_modal') {
      $this->commands[] = new HtmlCommand('#' . $section, $content);
    }
    else {
      $this->build[$section] = $content;
    }
  }

  /**
   * Get the AJAX commands to return to the blroser.
   * @return array
   *   Array of commands to return.
   */
  public function getCommands() {
    $commands = $this->commands;
    if (!empty($this->endCommand)) {
      $commands[] = $this->endCommand;
    }
    $this->commands = [];
    return $commands;
  }

  /**
   * \Add an ajax controller command to the stack.
   * @param $command
   */
  public function addCommand($command) {
    $this->commands[] = $command;
  }

  /**
   * Clear the commnds from the ajax buffer.
   */
  public function clearCommands() {
    $this->commands = [];
  }

  /**
   * Generate the ajax form replacement commands.
   * @param $section
   * @param $form
   * @return array
   */
  protected function generateAjaxReplace($section, $form) {
    $commands = [];

    // If the form build ID has changed, issue an Ajax command to update it.
    $build = $this->form_state
      ->getCompleteForm();
    if (isset($_POST['form_build_id']) && $_POST['form_build_id'] !== $build['#build_id']) {
      $commands[] = new UpdateBuildIdCommand($_POST['form_build_id'], $build['#build_id']);
    }

    // We need to return the part of the form (or some other content) that needs
    // to be re-rendered so the browser can update the page with changed
    // content. It is up to the #ajax['callback'] function of the element (may
    // or may not be a button) that triggered the Ajax request to determine what
    // needs to be rendered.
    $callback = NULL;
    $wrapper = NULL;
    $ajax_callback = NULL;

    // If we have an ajax callback assume we're doing a drupal style ajax replacment
    if (($triggering_element = $this->form_state
      ->getTriggeringElement()) && isset($triggering_element['#ajax']['callback'])) {
      $callback = $ajax_callback = $triggering_element['#ajax']['callback'];
      $wrapper = $triggering_element['#ajax']['wrapper'];
    }

    // Determine if there is a callback.
    $callback = $this->form_state
      ->prepareCallback($callback);
    if ($callback) {
      if (empty($callback) || !is_callable($callback)) {
        $commands[] = new HtmlCommand('#' . $section, $form);
      }
      $result = call_user_func_array($callback, [
        &$form,
        &$this->form_state,
      ]);

      // At this point we know callback returned a render element. If the
      // element is part of the group (#group is set on it) it won't be rendered
      // unless we remove #group from it. This is caused by
      // \Drupal\Core\Render\Element\RenderElement::preRenderGroup(), which
      // prevents all members of groups from being rendered directly.
      if (is_array($result)) {
        if (!empty($result['#group'])) {
          unset($result['#group']);
        }
        if ($wrapper) {
          $commands[] = new ReplaceCommand('#' . $wrapper, $result);
        }
        else {

          // we don't have a wrapper so assume form section replacement
          $commands[] = new HtmlCommand('#' . $section, $form);
        }
      }
    }
    else {

      // No ajax callback implies we are doing a normal full form replacment.
      $commands[] = new HtmlCommand('#' . $section, $form);
    }
    return $commands;
  }

  /**
   * Retrieve a drupal form for inclusion in the app.
   * Will load based on controlllerr's jsMode property as either
   * an ajax command or as an inline form.
   *
   * @param string $section
   *   The section of the template in which to render the form.
   * @param string $class
   *   The class name of the form to render.
   */
  protected function getForm($section, $class) {
    $modal = $this->is_modal_form;
    $this->prior_section = $this->section;
    $this->section = $section;
    $this->is_modal_form = FALSE;

    // We need to skip the second redering of this form in a route because
    // it may lead to an ajax error.  It should already have been rendered
    // in the form processing phase.
    if (empty($_POST['form_id']) || $class::FORM_ID != $this->post_form_id) {
      $content = \Drupal::formBuilder()
        ->getForm($class);
      if ($this->jsMode != 'nojs' && $this->jsMode != 'drupal_modal') {
        $this->commands = array_merge($this->commands, $this
          ->generateAjaxReplace($section, $content));
      }
      else {
        $this->build[$section] = $content;
      }
      $this->is_modal_form = $modal;
      $this->section = $this->prior_section;
    }
  }

  /**
   * @param string $section
   *   The class name of the form to render.
   * @param string $class
   *   The class name of the form to get.
   * @param string $title
   *   The title to be used on the modal.
   * @param array $options
   *   Array of Jquery UI Dialog options as described at
   *   http://api.jqueryui.com/dialog
   *
   */
  protected function getModalForm($section, $class, $title, $options = []) {
    $modal = $this->is_modal_form;
    $this->is_modal_form = TRUE;
    if (empty($_POST['form_id']) || $class::FORM_ID != $this->post_form_id) {
      $content = \Drupal::formBuilder()
        ->getForm($class);
      if ($this->jsMode != 'nojs') {
        if (!$this->modal_added) {
          $this->commands[] = new OpenModalDialogCommand($title, $content, $options);
          $this->modal_added = TRUE;

          // If autoResize is not manually disabled draggable will always be disabled
          // by drupal javascript.  So we add a command to enable it manually after
          // the form is initially sized.
          if (!isset($options['autoResize']) && !empty($options['draggable'])) {
            $this->commands[] = new InvokeCommand('#drupal-modal', 'eModalDraggable');
          }
        }
      }
      else {
        $this->build[$section] = $content;
      }
    }
    $this->is_modal_form = $modal;
  }

  /**
   * Render a forena report.
   * @param string $section
   *   The id of the section where the report goes.
   * @param string $report
   *   The name of the report to render.
   */
  protected function report($section, $report) {

    /** @var Forena $forena */
    $forena = \Drupal::service('forena.reports');
    $content = $forena
      ->report($report, $this->parms);
    if ($this->jsMode != 'nojs' && $this->jsMode != 'drupal_modal') {
      $this->commands[] = new HtmlCommand('#' . $section, $content);
    }
    else {
      $this->build[$section] = $content;
    }
  }
  public function runReport($report) {

    /** @var Forena $forena */
    $forena = \Drupal::service('forena.reports');
    return $forena
      ->runReport($report, $this->parms);
  }

  /**
   * Render a forena report in a modal
   * @param string $section
   *   The id of the section where the report goes.
   * @param string $title
   *   Title of modal window
   * @param string $report
   *   The name of the report to render.
   * @param array $options
   *   Modal dialog options.
   */
  protected function modalReport($section, $report, $title = '', $options = []) {

    /** @var Forena $forena */
    $forena = \Drupal::service('forena.reports');
    if ($this->jsMode == 'nojs') {
      $this
        ->report($section, $report);
    }
    else {
      $this->modal_content = $forena
        ->report($report, $this->parms);
      if (!isset($options['draggable'])) {
        $options['draggable'] = TRUE;
      }
      $this
        ->getModalForm($section, ReportModalForm::class, $title, $options);
    }
  }

  /**
   * Set the url for the controller, including current parameters.
   * @param string $action
   *   The relative action to set
   * @param string $title
   *   The browser title.
   * @param array|NULL $parms
   *   Parmaters.  If null speicificied then use the parms properties
   *   of the cotnroller.
   */
  public function setUrl($action, $title = NULL, $parms = NULL) {
    $query = "";
    if ($parms === NULL) {
      $parms = $this->parms;
    }
    if ($parms) {
      unset($parms['form_id']);
      $query = http_build_query($parms);
    }
    if ($query) {
      $action .= "?{$query}";
    }
    $this->endCommand = new InvokeCommand('html', 'forenaAjaxChangeUrl', [
      $action,
      $title,
    ]);
  }

  /**
   * Set the final command to be run as part of the ajax replacment.
   *
   * @param object $command
   *   AjaxCommand object.  There doesn't appear to be a base ajax command type
   *   for drupal.
   */
  public function setEndCommand($command) {
    $this->endCommand = $command;
  }

  /**
   * Initialize the layout report.
   */
  public function initLayout() {
    $this->build = [];
  }
  public function __sleep() {
    return [];
  }

}

Classes