Skip to content
ViewExecutable.php 65.4 KiB
Newer Older
Earl Miles's avatar
Earl Miles committed
<?php

/**
 * @file
 * Definition of Drupal\views\ViewExecutable.
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Form\FormState;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\views\Plugin\views\display\DisplayRouterInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewEntityInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
Earl Miles's avatar
Earl Miles committed
/**
Earl Miles's avatar
Earl Miles committed
 * An object to contain all of the data to generate a view, plus the member
 * functions to build the view query, execute the query and render the output.
 */
class ViewExecutable implements \Serializable {
Earl Miles's avatar
Earl Miles committed
  /**
   * The config entity in which the view is stored.
   * @todo Group with other static properties.
   *
   * @var bool
   */
  public $built = FALSE;

  /**
   * Whether the view has been executed/query has been run.
   *
   * @todo Group with other static properties.
   *
   * @var bool
   */
  public $executed = FALSE;

  /**
   * Any arguments that have been passed into the view.
   *
   * @var array
   */
  public $args = array();

  /**
   * An array of build info.
   *
   * @var array
   */
  public $build_info = array();

  /**
   * Whether this view uses AJAX.
   *
   * @var bool
   */
  /**
   * Where the results of a query will go.
   *
   * The array must use a numeric index starting at 0.
   *
Earl Miles's avatar
Earl Miles committed

  // May be used to override the current pager info.

  /**
   * The current page. If the view uses pagination.
   *
   * @var int
   */
  /**
   * Feed icons attached to the view.
   *
   * @var array
   */
  public $feedIcons = array();

Earl Miles's avatar
Earl Miles committed
  // Exposed widget input

   * All the form data from $form_state->getValues().
  /**
   * An array of input values from exposed forms.
   *
   * @var array
   */
  protected $exposed_input = array();
   * Exposed widget input directly from the $form_state->getValues().
   *
   * @var array
   */
  public $exposed_raw_input = array();

  /**
   * Used to store views that were previously running if we recurse.
   *
   * @var \Drupal\views\ViewExecutable[]
   */
  public $old_view = array();

  /**
   * To avoid recursion in views embedded into areas.
   *
   * @var \Drupal\views\ViewExecutable[]
   */
  public $parent_views = array();

  /**
   * Whether this view is an attachment to another view.
   *
   * @var bool
   */
  public $is_attachment = NULL;
Earl Miles's avatar
Earl Miles committed

  /**
   * Identifier of the current display.
   *
   * @var string
   */
   * @var \Drupal\views\Plugin\views\query\QueryPluginBase
  /**
   * The used pager plugin used by the current executed view.
   *
   * @var \Drupal\views\Plugin\views\pager\PagerPluginBase
Earl Miles's avatar
Earl Miles committed
   * The current used display plugin.
   *
   * @var \Drupal\views\Plugin\views\display\DisplayPluginBase
  /**
   * The list of used displays of the view.
   *
   * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase
   * objects.
   *
   * @var \Drupal\views\DisplayPluginCollection
Earl Miles's avatar
Earl Miles committed
  /**
   * The current used style plugin.
   *
   * @var \Drupal\views\Plugin\views\style\StylePluginBase
  /**
   * The current used row plugin, if the style plugin supports row plugins.
   *
   * @var \Drupal\views\Plugin\views\row\RowPluginBase
   */
  public $rowPlugin;

Earl Miles's avatar
Earl Miles committed
  /**
   * Stores the current active row while rendering.
   *
   * @var int
   */
Earl Miles's avatar
Earl Miles committed
   * Allow to override the url of the current view.
   *
Earl Miles's avatar
Earl Miles committed

  /**
   * Allow to override the path used for generated urls.
   *
   * @var string
   */
Earl Miles's avatar
Earl Miles committed

  /**
   * Allow to override the used database which is used for this query.
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the field handlers which are initialized on this view.
   * @var \Drupal\views\Plugin\views\field\FieldPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the argument handlers which are initialized on this view.
   * @var \Drupal\views\Plugin\views\argument\ArgumentPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the sort handlers which are initialized on this view.
   * @var \Drupal\views\Plugin\views\sort\SortPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the filter handlers which are initialized on this view.
   * @var \Drupal\views\Plugin\views\filter\FilterPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the relationship handlers which are initialized on this view.
   * @var \Drupal\views\Plugin\views\relationship\RelationshipPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the area handlers for the header which are initialized on this view.
   * @var \Drupal\views\Plugin\views\area\AreaPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the area handlers for the footer which are initialized on this view.
   * @var \Drupal\views\Plugin\views\area\AreaPluginBase[]
Earl Miles's avatar
Earl Miles committed

  /**
   * Stores the area handlers for the empty text which are initialized on this view.
   *
   * An array containing Drupal\views\Plugin\views\area\AreaPluginBase objects.
   *
   * @var \Drupal\views\Plugin\views\area\AreaPluginBase[]
   * @var \Symfony\Component\HttpFoundation\Response
  /**
   * Stores the current request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * Does this view already have loaded it's handlers.
   *
   * @todo Group with other static properties.
   *
   * @var bool
   */
  public $inited;

  /**
   * The rendered output of the exposed form.
   *
   * @var string
   */
  public $exposed_widgets;

  /**
   * If this view has been previewed.
   *
   * @var bool
   */
  public $preview;

  /**
   * Force the query to calculate the total number of results.
   *
   * @todo Move to the query.
   *
   * @var bool
   */
  public $get_total_rows;

  /**
   * Indicates if the sorts have been built.
   *
   * @todo Group with other static properties.
   *
   * @var bool
   */
  public $build_sort;

  /**
   * Stores the many-to-one tables for performance.
   *
   * @var array
   */
  public $many_to_one_tables;

  /**
   * A unique identifier which allows to update multiple views output via js.
   *
   * @var string
   */
  public $dom_id;

  /**
   * A render array container to store render related information.
   *
   * For example you can alter the array and attach some asset library or JS
   * settings via the #attached key. This is the required way to add custom
   * CSS or JS.
  public $element = [
    '#attached' => [
      'library' => [],
      'drupalSettings' => [],
    ]
  ];
  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $user;

  /**
   * Should the admin links be shown on the rendered view.
   *
   * @var bool
   */
  protected $showAdminLinks;

  /**
   * The views data.
   *
   * @var \Drupal\views\ViewsData
   */
  protected $viewsData;

  /**
   * The route provider.
   *
   * @var \Drupal\Core\Routing\RouteProviderInterface
   */
  protected $routeProvider;

   * @param \Drupal\views\ViewEntityInterface $storage
   *   The view config entity the actual information is stored on.
   * @param \Drupal\Core\Session\AccountInterface $user
   *   The current user.
   * @param \Drupal\views\ViewsData $views_data
   *   The views data.
   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
   *   The route provider.
  public function __construct(ViewEntityInterface $storage, AccountInterface $user, ViewsData $views_data, RouteProviderInterface $route_provider) {
    // Reference the storage and the executable to each other.
    $this->storage = $storage;
    $this->storage->set('executable', $this);
    $this->routeProvider = $route_provider;
    $this->element['#attached']['library'][] = 'views/views.module';
   * Returns the identifier.
   *
   * @return string|null
   *   The entity identifier, or NULL if the object does not yet have an
   *   identifier.
   */
  public function id() {
    return $this->storage->id();
  }

  /**
   * Saves the view.
  public function save() {
    $this->storage->save();
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Set the arguments that come to this view. Usually from the URL
   * but possibly from elsewhere.
   */
  public function setArguments(array $args) {
    // The array keys of the arguments will be incorrect if set by
    // views_embed_view() or \Drupal\views\ViewExecutable:preview().
    $this->args = array_values($args);
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Change/Set the current page for the pager.
   *
   * @param int $page
   *   The current page.
   * @param bool $keep_cacheability
   *   (optional) Keep the cacheability. By default we mark the view as not
   *   cacheable. The reason for this parameter is that we do not know what the
   *   passed in value varies by. For example, it could be per role. Defaults to
   *   FALSE.
  public function setCurrentPage($page, $keep_cacheability = FALSE) {
Earl Miles's avatar
Earl Miles committed
    $this->current_page = $page;
    if (!$keep_cacheability) {
      $this->element['#cache']['max-age'] = 0;
    }

    // If the pager is already initialized, pass it through to the pager.
      return $this->pager->setCurrentPage($page);
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Get the current page from the pager.
   */
    // If the pager is already initialized, pass it through to the pager.
      return $this->pager->getCurrentPage();

    if (isset($this->current_page)) {
      return $this->current_page;
    }
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Get the items per page from the pager.
   */
    // If the pager is already initialized, pass it through to the pager.
      return $this->pager->getItemsPerPage();

    if (isset($this->items_per_page)) {
      return $this->items_per_page;
    }
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Set the items per page on the pager.
   *
   * @param int $items_per_page
   *   The items per page.
   * @param bool $keep_cacheability
   *   (optional) Keep the cacheability. By default we mark the view as not
   *   cacheable. The reason for this parameter is that we do not know what the
   *   passed in value varies by. For example, it could be per role. Defaults to
   *   FALSE.
  public function setItemsPerPage($items_per_page, $keep_cacheability = FALSE) {
Earl Miles's avatar
Earl Miles committed
    $this->items_per_page = $items_per_page;

    // If the pager is already initialized, pass it through to the pager.
      $this->pager->setItemsPerPage($items_per_page);

    if (!$keep_cacheability) {
      $this->element['#cache']['max-age'] = 0;
    }
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Get the pager offset from the pager.
   */
    // If the pager is already initialized, pass it through to the pager.
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Set the offset on the pager.
   *
   * @param int $offset
   *   The pager offset.
   * @param bool $keep_cacheability
   *   (optional) Keep the cacheability. By default we mark the view as not
   *   cacheable. The reason for this parameter is that we do not know what the
   *   passed in value varies by. For example, it could be per role. Defaults to
   *   FALSE.
  public function setOffset($offset, $keep_cacheability = FALSE) {
Earl Miles's avatar
Earl Miles committed
    $this->offset = $offset;

    // If the pager is already initialized, pass it through to the pager.

    if (!$keep_cacheability) {
      $this->element['#cache']['max-age'] = 0;
    }
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Determine if the pager actually uses a pager.
   */
   * Sets whether or not AJAX should be used.
   *
   * If AJAX is used, paging, tablesorting and exposed filters will be fetched
   * via an AJAX call rather than a page refresh.
   *
   * @param bool $use_ajax
   *   TRUE if AJAX should be used, FALSE otherwise.
   */
  public function setAjaxEnabled($ajax_enabled) {
    $this->ajaxEnabled = (bool) $ajax_enabled;
  }

  /**
   * Whether or not AJAX should be used.
   *
   * @see \Drupal\views\ViewExecutable::setAjaxEnabled().
   *
   * @return bool
  public function ajaxEnabled() {
    return $this->ajaxEnabled;
Earl Miles's avatar
Earl Miles committed
  }

  /**
   * Set the exposed filters input to an array. If unset they will be taken
   * from \Drupal::request()->query when the time comes.
Earl Miles's avatar
Earl Miles committed
    $this->exposed_input = $filters;
  }

  /**
   * Figure out what the exposed input for this view is.
   */
    // Fill our input either from \Drupal::request()->query or from something
    // previously set on the view.
Earl Miles's avatar
Earl Miles committed
    if (empty($this->exposed_input)) {
      // Ensure that we can call the method at any point in time.
      $this->initDisplay();

      $this->exposed_input = \Drupal::request()->query->all();
Earl Miles's avatar
Earl Miles committed
      // unset items that are definitely not our input:
      foreach (array('page', 'q') as $key) {
        if (isset($this->exposed_input[$key])) {
          unset($this->exposed_input[$key]);
        }
      }

      // If we have no input at all, check for remembered input via session.

      // If filters are not overridden, store the 'remember' settings on the
      // default display. If they are, store them on this display. This way,
      // multiple displays in the same view can share the same filters and
      // remember settings.
      $display_id = ($this->display_handler->isDefaulted('filters')) ? 'default' : $this->current_display;
      if (empty($this->exposed_input) && !empty($_SESSION['views'][$this->storage->id()][$display_id])) {
        $this->exposed_input = $_SESSION['views'][$this->storage->id()][$display_id];
Earl Miles's avatar
Earl Miles committed
      }
    }

    return $this->exposed_input;
  }

  /**
   * Set the display for this view and initialize the display handler.
   */
Earl Miles's avatar
Earl Miles committed
    if (isset($this->current_display)) {
      return TRUE;
    }

    $this->displayHandlers = new DisplayPluginCollection($this, Views::pluginManager('display'));
Earl Miles's avatar
Earl Miles committed

    $this->current_display = 'default';
    $this->display_handler = $this->displayHandlers->get('default');
Earl Miles's avatar
Earl Miles committed

    return TRUE;
  }

  /**
   * Get the first display that is accessible to the user.
   *
Earl Miles's avatar
Earl Miles committed
   *   Either a single display id or an array of display ids.
   *
   * @return string
   *   The first accessible display id, at least default.
Earl Miles's avatar
Earl Miles committed
    if (!is_array($displays)) {
      return $displays;
    }

Earl Miles's avatar
Earl Miles committed

    foreach ($displays as $display_id) {
      if ($this->displayHandlers->get($display_id)->access($this->user)) {
Earl Miles's avatar
Earl Miles committed
        return $display_id;
      }
    }

    return 'default';
  }

  /**
   * Gets the current display plugin.
   *
   * @return \Drupal\views\Plugin\views\display\DisplayPluginBase
   */
  public function getDisplay() {
    if (!isset($this->display_handler)) {
      $this->initDisplay();
    }

    return $this->display_handler;
  }

   * @param string $display_id
   *   The ID of the display to mark as current.
   *
   * @return bool
   *   TRUE if the display was correctly set, FALSE otherwise.
  public function setDisplay($display_id = NULL) {
    // If we have not already initialized the display, do so.
    if (!isset($this->current_display)) {
      // This will set the default display and instantiate the default display
      // plugin.
    // If no display ID is passed, we either have initialized the default or
    // already have a display set.
    if (!isset($display_id)) {
      return TRUE;
    $display_id = $this->chooseDisplay($display_id);
Earl Miles's avatar
Earl Miles committed

    // Ensure the requested display exists.
    if (!$this->displayHandlers->has($display_id)) {
      debug(format_string('setDisplay() called with invalid display ID "@display".', array('@display' => $display_id)));
      return FALSE;
    // Reset if the display has changed. It could be called multiple times for
    // the same display, especially in the UI.
    if ($this->current_display != $display_id) {
      // Set the current display.
      $this->current_display = $display_id;

      // Reset the style and row plugins.
      $this->style_plugin = NULL;
      $this->plugin_name = NULL;
      $this->rowPlugin = NULL;
    }

    if ($display = $this->displayHandlers->get($display_id)) {
      // Set a shortcut.
      $this->display_handler = $display;
      return TRUE;
    }
  /**
   * Creates a new display and a display handler instance for it.
   *
   * @param string $plugin_id
   *   (optional) The plugin type from the Views plugin annotation. Defaults to
   *   'page'.
   * @param string $title
   *   (optional) The title of the display. Defaults to NULL.
   * @param string $id
   *   (optional) The ID to use, e.g., 'default', 'page_1', 'block_2'. Defaults
   *   to NULL.
   *
   * @return \Drupal\views\Plugin\views\display\DisplayPluginBase
   *   A new display plugin instance if executable is set, the new display ID
   *   otherwise.
   */
  public function newDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
    $this->initDisplay();

    $id = $this->storage->addDisplay($plugin_id, $title, $id);
    $display = $this->displayHandlers->get($id);
    $display->newDisplay();
    return $display;
  /**
   * Gets the current style plugin.
   *
   * @return \Drupal\views\Plugin\views\style\StylePluginBase
   */
  public function getStyle() {
    if (!isset($this->style_plugin)) {
      $this->initStyle();
    }

    return $this->style_plugin;
  }

Earl Miles's avatar
Earl Miles committed
  /**
   * Find and initialize the style plugin.
   *
   * Note that arguments may have changed which style plugin we use, so
   * check the view object first, then ask the display handler.
   */
Earl Miles's avatar
Earl Miles committed
    if (isset($this->style_plugin)) {
    $this->style_plugin = $this->display_handler->getPlugin('style');
Earl Miles's avatar
Earl Miles committed

    if (empty($this->style_plugin)) {
      return FALSE;
    }

    return TRUE;
  }

  /**
   * Acquire and attach all of the handlers.
   */
Earl Miles's avatar
Earl Miles committed
    if (empty($this->inited)) {
      foreach ($this::getHandlerTypes() as $key => $info) {
Earl Miles's avatar
Earl Miles committed
      }
      $this->inited = TRUE;
    }
  }

  /**
   * Get the current pager plugin.
   *
   * @return \Drupal\views\Plugin\views\pager\PagerPluginBase
   */
  public function getPager() {
    if (!isset($this->pager)) {
      $this->initPager();
    }

    return $this->pager;
  }

Earl Miles's avatar
Earl Miles committed
  /**
   * Initialize the pager
   *
   * Like style initialization, pager initialization is held until late
   * to allow for overrides.
   */
      $this->pager = $this->display_handler->getPlugin('pager');
        $this->pager->setCurrentPage($this->current_page);
Earl Miles's avatar
Earl Miles committed
      }

      // These overrides may have been set earlier via $view->set_*
      // functions.
      if (isset($this->items_per_page)) {
        $this->pager->setItemsPerPage($this->items_per_page);
Earl Miles's avatar
Earl Miles committed
      }

      if (isset($this->offset)) {
        $this->pager->setOffset($this->offset);
  public function renderPager($exposed_input) {
    if (!empty($this->pager) && $this->pager->usePager()) {
Earl Miles's avatar
Earl Miles committed
  /**
   * Create a list of base tables eligible for this view. Used primarily
   * for the UI. Display must be already initialized.
   */
Earl Miles's avatar
Earl Miles committed
    $base_tables = array(
      $this->storage->get('base_table') => TRUE,
Earl Miles's avatar
Earl Miles committed
      '#global' => TRUE,
    );

    foreach ($this->display_handler->getHandlers('relationship') as $handler) {
Earl Miles's avatar
Earl Miles committed
      $base_tables[$handler->definition['base']] = TRUE;
    }
    return $base_tables;
  }

  /**
   * Run the preQuery() on all active handlers.
    foreach ($this::getHandlerTypes() as $key => $info) {
Earl Miles's avatar
Earl Miles committed
      $handlers = &$this->$key;
      $position = 0;
      foreach ($handlers as $id => $handler) {
        $handlers[$id]->position = $position;
   * Run the postExecute() on all active handlers.
    foreach ($this::getHandlerTypes() as $key => $info) {
Earl Miles's avatar
Earl Miles committed
      $handlers = &$this->$key;
      foreach ($handlers as $id => $handler) {
        $handlers[$id]->postExecute($this->result);
Earl Miles's avatar
Earl Miles committed
      }
    }
  }

  /**
   * Attach all of the handlers for each type.
   *
   * @param $key
   *   One of 'argument', 'field', 'sort', 'filter', 'relationship'
   * @param $info
   *   The $info from getHandlerTypes for this object.
  protected function _initHandler($key, $info) {
Earl Miles's avatar
Earl Miles committed
    // Load the requested items from the display onto the object.
    $this->$key = &$this->display_handler->getHandlers($key);
Earl Miles's avatar
Earl Miles committed

    // This reference deals with difficult PHP indirection.
    $handlers = &$this->$key;

    // Run through and test for accessibility.
    foreach ($handlers as $id => $handler) {
Earl Miles's avatar
Earl Miles committed
        unset($handlers[$id]);
      }
    }
  }

  /**
   * Build all the arguments.
   */
Earl Miles's avatar
Earl Miles committed
    // Initially, we want to build sorts and fields. This can change, though,
    // if we get a summary view.
    if (empty($this->argument)) {
      return TRUE;
    }

    // build arguments.
    $position = -1;
    $substitutions = array();
    $status = TRUE;

    // Get the title.
    $title = $this->display_handler->getOption('title');

Earl Miles's avatar
Earl Miles committed
    // Iterate through each argument and process.
    foreach ($this->argument as $id => $arg) {
      $position++;
Earl Miles's avatar
Earl Miles committed

      if ($argument->broken()) {