NodeSearch.php 22.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\node\Plugin\Search\NodeSearch.
 */

namespace Drupal\node\Plugin\Search;

10
use Drupal\Component\Utility\SafeMarkup;
11
use Drupal\Component\Utility\String;
12
use Drupal\Core\Access\AccessResult;
13 14 15
use Drupal\Core\Config\Config;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectExtender;
16
use Drupal\Core\Database\StatementInterface;
17
use Drupal\Core\Entity\EntityManagerInterface;
18
use Drupal\Core\Extension\ModuleHandlerInterface;
19
use Drupal\Core\Form\FormStateInterface;
20
use Drupal\Core\Language\LanguageInterface;
21
use Drupal\Core\Session\AccountInterface;
22
use Drupal\Core\Access\AccessibleInterface;
23
use Drupal\Core\Database\Query\Condition;
24
use Drupal\node\NodeInterface;
25
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
26
use Drupal\search\Plugin\SearchIndexingInterface;
27
use Drupal\Search\SearchQuery;
28 29 30 31 32 33 34
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Handles searching for node entities using the Search module index.
 *
 * @SearchPlugin(
 *   id = "node_search",
35
 *   title = @Translation("Content")
36 37
 * )
 */
38
class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
39 40 41 42 43 44 45 46 47 48 49

  /**
   * A database connection object.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * An entity manager object.
   *
50
   * @var \Drupal\Core\Entity\EntityManagerInterface
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
   */
  protected $entityManager;

  /**
   * A module manager object.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * A config object for 'search.settings'.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $searchSettings;

  /**
   * The Drupal account to use for checking for access to advanced search.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

75 76 77 78 79 80 81
  /**
   * An array of additional rankings from hook_ranking().
   *
   * @var array
   */
  protected $rankings;

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
  /**
   * The list of options and info for advanced search filters.
   *
   * Each entry in the array has the option as the key and and for its value, an
   * array that determines how the value is matched in the database query. The
   * possible keys in that array are:
   * - column: (required) Name of the database column to match against.
   * - join: (optional) Information on a table to join. By default the data is
   *   matched against the {node_field_data} table.
   * - operator: (optional) OR or AND, defaults to OR.
   *
   * @var array
   */
  protected $advanced = array(
    'type' => array('column' => 'n.type'),
97
    'language' => array('column' => 'i.langcode'),
98 99 100 101 102 103 104
    'author' => array('column' => 'n.uid'),
    'term' => array('column' => 'ti.tid', 'join' => array('table' => 'taxonomy_index', 'alias' => 'ti', 'condition' => 'n.nid = ti.nid')),
  );

  /**
   * {@inheritdoc}
   */
105
  static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
106 107 108 109 110
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('database'),
111
      $container->get('entity.manager'),
112 113
      $container->get('module_handler'),
      $container->get('config.factory')->get('search.settings'),
114
      $container->get('current_user')
115 116 117 118 119 120 121 122 123 124
    );
  }

  /**
   * Constructs a \Drupal\node\Plugin\Search\NodeSearch object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
125
   * @param mixed $plugin_definition
126 127 128
   *   The plugin implementation definition.
   * @param \Drupal\Core\Database\Connection $database
   *   A database connection object.
129
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
130 131 132 133 134 135 136 137
   *   An entity manager object.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   A module manager object.
   * @param \Drupal\Core\Config\Config $search_settings
   *   A config object for 'search.settings'.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The $account object to use for checking for access to advanced search.
   */
138
  public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, AccountInterface $account = NULL) {
139 140 141 142 143 144 145 146 147 148 149
    $this->database = $database;
    $this->entityManager = $entity_manager;
    $this->moduleHandler = $module_handler;
    $this->searchSettings = $search_settings;
    $this->account = $account;
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
150 151 152
  public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
    $result = AccessResult::allowedIfHasPermission($account, 'access content');
    return $return_as_object ? $result : $result->isAllowed();
153 154
  }

155 156 157 158 159 160 161 162 163 164 165
  /**
   * {@inheritdoc}
   */
  public function isSearchExecutable() {
    // Node search is executable if we have keywords or an advanced parameter.
    // At least, we should parse out the parameters and see if there are any
    // keyword matches in that case, rather than just printing out the
    // "Please enter keywords" message.
    return !empty($this->keywords) || (isset($this->searchParameters['f']) && count($this->searchParameters['f']));
  }

166 167 168 169
  /**
   * {@inheritdoc}
   */
  public function execute() {
170 171 172 173 174 175
    if ($this->isSearchExecutable()) {
      $results = $this->findResults();

      if ($results) {
        return $this->prepareResults($results);
      }
176
    }
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

    return array();
  }

  /**
   * Queries to find search results, and sets status messages.
   *
   * This method can assume that $this->isSearchExecutable() has already been
   * checked and returned TRUE.
   *
   * @return \Drupal\Core\Database\StatementInterface|null
   *   Results from search query execute() method, or NULL if the search
   *   failed.
   */
  protected function findResults() {
192 193 194 195
    $keys = $this->keywords;

    // Build matching conditions.
    $query = $this->database
196
      ->select('search_index', 'i', array('target' => 'replica'))
197 198 199 200 201 202 203 204
      ->extend('Drupal\search\SearchQuery')
      ->extend('Drupal\Core\Database\Query\PagerSelectExtender');
    $query->join('node_field_data', 'n', 'n.nid = i.sid');
    $query->condition('n.status', 1)
      ->addTag('node_access')
      ->searchExpression($keys, $this->getPluginId());

    // Handle advanced search filters in the f query string.
205 206
    // \Drupal::request()->query->get('f') is an array that looks like this in
    // the URL: ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en
207 208
    // So $parameters['f'] looks like:
    // array('type:page', 'term:27', 'term:13', 'langcode:en');
209 210
    // We need to parse this out into query conditions, some of which go into
    // the keywords string, and some of which are separate conditions.
211 212 213 214 215 216 217 218 219 220 221 222
    $parameters = $this->getParameters();
    if (!empty($parameters['f']) && is_array($parameters['f'])) {
      $filters = array();
      // Match any query value that is an expected option and a value
      // separated by ':' like 'term:27'.
      $pattern = '/^(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i';
      foreach ($parameters['f'] as $item) {
        if (preg_match($pattern, $item, $m)) {
          // Use the matched value as the array key to eliminate duplicates.
          $filters[$m[1]][$m[2]] = $m[2];
        }
      }
223

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
      // Now turn these into query conditions. This assumes that everything in
      // $filters is a known type of advanced search.
      foreach ($filters as $option => $matched) {
        $info = $this->advanced[$option];
        // Insert additional conditions. By default, all use the OR operator.
        $operator = empty($info['operator']) ? 'OR' : $info['operator'];
        $where = new Condition($operator);
        foreach ($matched as $value) {
          $where->condition($info['column'], $value);
        }
        $query->condition($where);
        if (!empty($info['join'])) {
          $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']);
        }
      }
    }

    // Add the ranking expressions.
    $this->addNodeRankings($query);

244
    // Run the query.
245 246 247 248
    $find = $query
      // Add the language code of the indexed item to the result of the query,
      // since the node will be rendered using the respective language.
      ->fields('i', array('langcode'))
249 250 251 252
      // And since SearchQuery makes these into GROUP BY queries, if we add
      // a field, for PostgreSQL we also need to make it an aggregate or a
      // GROUP BY. In this case, we want GROUP BY.
      ->groupBy('i.langcode')
253 254 255
      ->limit(10)
      ->execute();

256 257 258 259 260 261 262 263 264 265 266 267
    // Check query status and set messages if needed.
    $status = $query->getStatus();

    if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
      drupal_set_message($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $this->searchSettings->get('and_or_limit'))), 'warning');
    }

    if ($status & SearchQuery::LOWER_CASE_OR) {
      drupal_set_message($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'), 'warning');
    }

    if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
268
      drupal_set_message($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.'), 'warning');
269 270
    }

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
    return $find;
  }

  /**
   * Prepares search results for rendering.
   *
   * @param \Drupal\Core\Database\StatementInterface $found
   *   Results found from a successful search query execute() method.
   *
   * @return array
   *   Array of search result item render arrays (empty array if no results).
   */
  protected function prepareResults(StatementInterface $found) {
    $results = array();

286
    $node_storage = $this->entityManager->getStorage('node');
287
    $node_render = $this->entityManager->getViewBuilder('node');
288
    $keys = $this->keywords;
289

290
    foreach ($found as $item) {
291
      // Render the node.
292
      /** @var \Drupal\node\NodeInterface $node */
293
      $node = $node_storage->load($item->sid)->getTranslation($item->langcode);
294 295 296 297
      $build = $node_render->view($node, 'search_result', $item->langcode);
      unset($build['#theme']);

      // Fetch comment count for snippet.
298 299 300 301
      $node->rendered = SafeMarkup::set(
        drupal_render($build) . ' ' .
        SafeMarkup::escape($this->moduleHandler->invoke('comment', 'node_update_index', array($node, $item->langcode)))
      );
302 303 304 305 306 307

      $extra = $this->moduleHandler->invokeAll('node_search_result', array($node, $item->langcode));

      $language = language_load($item->langcode);
      $username = array(
        '#theme' => 'username',
308
        '#account' => $node->getOwner(),
309 310
      );
      $results[] = array(
311
        'link' => $node->url('canonical', array('absolute' => TRUE, 'language' => $language)),
312
        'type' => String::checkPlain($this->entityManager->getStorage('node_type')->load($node->bundle())->label()),
313
        'title' => $node->label(),
314 315 316 317 318 319
        'user' => drupal_render($username),
        'date' => $node->getChangedTime(),
        'node' => $node,
        'extra' => $extra,
        'score' => $item->calculated_score,
        'snippet' => search_excerpt($keys, $node->rendered, $item->langcode),
320
        'langcode' => $node->language()->getId(),
321 322 323 324 325 326
      );
    }
    return $results;
  }

  /**
327
   * Adds the configured rankings to the search query.
328 329 330 331 332
   *
   * @param $query
   *   A query object that has been extended with the Search DB Extender.
   */
  protected function addNodeRankings(SelectExtender $query) {
333
    if ($ranking = $this->getRankings()) {
334 335
      $tables = &$query->getTables();
      foreach ($ranking as $rank => $values) {
336 337
        if (isset($this->configuration['rankings'][$rank]) && !empty($this->configuration['rankings'][$rank])) {
          $node_rank = $this->configuration['rankings'][$rank];
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
          // If the table defined in the ranking isn't already joined, then add it.
          if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
            $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
          }
          $arguments = isset($values['arguments']) ? $values['arguments'] : array();
          $query->addScore($values['score'], $arguments, $node_rank);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function updateIndex() {
353 354
    // Interpret the cron limit setting as the maximum number of nodes to index
    // per cron run.
355 356
    $limit = (int) $this->searchSettings->get('index.cron_limit');

357
    $result = $this->database->queryRange("SELECT n.nid, MAX(sd.reindex) FROM {node} n LEFT JOIN {search_dataset} sd ON sd.sid = n.nid AND sd.type = :type WHERE sd.sid IS NULL OR sd.reindex <> 0 GROUP BY n.nid ORDER BY MAX(sd.reindex) is null DESC, MAX(sd.reindex) ASC, n.nid ASC", 0, $limit, array(':type' => $this->getPluginId()), array('target' => 'replica'));
358 359 360 361 362
    $nids = $result->fetchCol();
    if (!$nids) {
      return;
    }

363
    $node_storage = $this->entityManager->getStorage('node');
364 365 366 367 368 369 370 371
    foreach ($node_storage->loadMultiple($nids) as $node) {
      $this->indexNode($node);
    }
  }

  /**
   * Indexes a single node.
   *
372
   * @param \Drupal\node\NodeInterface $node
373 374
   *   The node to index.
   */
375
  protected function indexNode(NodeInterface $node) {
376
    $languages = $node->getTranslationLanguages();
377
    $node_render = $this->entityManager->getViewBuilder('node');
378 379

    foreach ($languages as $language) {
380
      $node = $node->getTranslation($language->getId());
381
      // Render the node.
382
      $build = $node_render->view($node, 'search_index', $language->getId());
383 384 385 386

      unset($build['#theme']);
      $node->rendered = drupal_render($build);

387
      $text = '<h1>' . String::checkPlain($node->label($language->getId())) . '</h1>' . $node->rendered;
388 389

      // Fetch extra data normally not visible.
390
      $extra = $this->moduleHandler->invokeAll('node_update_index', array($node, $language->getId()));
391 392 393 394
      foreach ($extra as $t) {
        $text .= $t;
      }

395 396
      // Update index, using search index "type" equal to the plugin ID.
      search_index($this->getPluginId(), $node->id(), $language->getId(), $text);
397 398 399 400 401 402
    }
  }

  /**
   * {@inheritdoc}
   */
403 404 405 406 407 408 409 410 411 412 413 414 415
  public function indexClear() {
    // All NodeSearch pages share a common search index "type" equal to
    // the plugin ID.
    search_index_clear($this->getPluginId());
  }

  /**
   * {@inheritdoc}
   */
  public function markForReindex() {
    // All NodeSearch pages share a common search index "type" equal to
    // the plugin ID.
    search_mark_for_reindex($this->getPluginId());
416 417 418 419 420 421 422
  }

  /**
   * {@inheritdoc}
   */
  public function indexStatus() {
    $total = $this->database->query('SELECT COUNT(*) FROM {node}')->fetchField();
423 424
    $remaining = $this->database->query("SELECT COUNT(DISTINCT n.nid) FROM {node} n LEFT JOIN {search_dataset} sd ON sd.sid = n.nid AND sd.type = :type WHERE sd.sid IS NULL OR sd.reindex <> 0", array(':type' => $this->getPluginId()))->fetchField();

425 426 427 428 429 430
    return array('remaining' => $remaining, 'total' => $total);
  }

  /**
   * {@inheritdoc}
   */
431
  public function searchFormAlter(array &$form, FormStateInterface $form_state) {
432
    // Add advanced search keyword-related boxes.
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
    $form['advanced'] = array(
      '#type' => 'details',
      '#title' => t('Advanced search'),
      '#attributes' => array('class' => array('search-advanced')),
      '#access' => $this->account && $this->account->hasPermission('use advanced search'),
    );
    $form['advanced']['keywords-fieldset'] = array(
      '#type' => 'fieldset',
      '#title' => t('Keywords'),
    );
    $form['advanced']['keywords'] = array(
      '#prefix' => '<div class="criterion">',
      '#suffix' => '</div>',
    );
    $form['advanced']['keywords-fieldset']['keywords']['or'] = array(
      '#type' => 'textfield',
      '#title' => t('Containing any of the words'),
      '#size' => 30,
      '#maxlength' => 255,
    );
    $form['advanced']['keywords-fieldset']['keywords']['phrase'] = array(
      '#type' => 'textfield',
      '#title' => t('Containing the phrase'),
      '#size' => 30,
      '#maxlength' => 255,
    );
    $form['advanced']['keywords-fieldset']['keywords']['negative'] = array(
      '#type' => 'textfield',
      '#title' => t('Containing none of the words'),
      '#size' => 30,
      '#maxlength' => 255,
    );

    // Add node types.
467
    $types = array_map(array('\Drupal\Component\Utility\String', 'checkPlain'), node_type_get_names());
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
    $form['advanced']['types-fieldset'] = array(
      '#type' => 'fieldset',
      '#title' => t('Types'),
    );
    $form['advanced']['types-fieldset']['type'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Only of the type(s)'),
      '#prefix' => '<div class="criterion">',
      '#suffix' => '</div>',
      '#options' => $types,
    );
    $form['advanced']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Advanced search'),
      '#prefix' => '<div class="action">',
      '#suffix' => '</div>',
      '#weight' => 100,
    );

    // Add languages.
    $language_options = array();
489
    $language_list = \Drupal::languageManager()->getLanguages(LanguageInterface::STATE_ALL);
490
    foreach ($language_list as $langcode => $language) {
491
      // Make locked languages appear special in the list.
492
      $language_options[$langcode] = $language->isLocked() ? t('- @name -', array('@name' => $language->getName())) : $language->getName();
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
    }
    if (count($language_options) > 1) {
      $form['advanced']['lang-fieldset'] = array(
        '#type' => 'fieldset',
        '#title' => t('Languages'),
      );
      $form['advanced']['lang-fieldset']['language'] = array(
        '#type' => 'checkboxes',
        '#title' => t('Languages'),
        '#prefix' => '<div class="criterion">',
        '#suffix' => '</div>',
        '#options' => $language_options,
      );
    }
  }

509 510
  /*
   * {@inheritdoc}
511
   */
512
  public function buildSearchUrlQuery(FormStateInterface $form_state) {
513 514
    // Read keyword and advanced search information from the form values,
    // and put these into the GET parameters.
515
    $keys = trim($form_state->getValue('keys'));
516

517 518
    // Collect extra filters.
    $filters = array();
519
    if ($form_state->hasValue('type') && is_array($form_state->getValue('type'))) {
520 521
      // Retrieve selected types - Form API sets the value of unselected
      // checkboxes to 0.
522
      foreach ($form_state->getValue('type') as $type) {
523 524 525 526 527 528
        if ($type) {
          $filters[] = 'type:' . $type;
        }
      }
    }

529 530
    if ($form_state->hasValue('term') && is_array($form_state->getValue('term'))) {
      foreach ($form_state->getValue('term') as $term) {
531 532 533
        $filters[] = 'term:' . $term;
      }
    }
534 535
    if ($form_state->hasValue('language') && is_array($form_state->getValue('language'))) {
      foreach ($form_state->getValue('language') as $language) {
536 537 538 539 540
        if ($language) {
          $filters[] = 'language:' . $language;
        }
      }
    }
541 542
    if ($form_state->getValue('or') != '') {
      if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state->getValue('or'), $matches)) {
543 544 545
        $keys .= ' ' . implode(' OR ', $matches[1]);
      }
    }
546 547
    if ($form_state->getValue('negative') != '') {
      if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state->getValue('negative'), $matches)) {
548 549 550
        $keys .= ' -' . implode(' -', $matches[1]);
      }
    }
551 552
    if ($form_state->getValue('phrase') != '') {
      $keys .= ' "' . str_replace('"', ' ', $form_state->getValue('phrase')) . '"';
553
    }
554 555 556 557 558 559
    $keys = trim($keys);

    // Put the keywords and advanced parameters into GET parameters. Make sure
    // to put keywords into the query even if it is empty, because the page
    // controller uses that to decide it's time to check for search results.
    $query = array('keys' => $keys);
560
    if ($filters) {
561
      $query['f'] = $filters;
562 563
    }

564
    return $query;
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
  }

  /**
   * Gathers ranking definitions from hook_ranking().
   *
   * @return array
   *   An array of ranking definitions.
   */
  protected function getRankings() {
    if (!$this->rankings) {
      $this->rankings = $this->moduleHandler->invokeAll('ranking');
    }
    return $this->rankings;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    $configuration = array(
      'rankings' => array(),
    );
    return $configuration;
588 589 590 591 592
  }

  /**
   * {@inheritdoc}
   */
593
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
594 595 596 597
    // Output form for defining rank factor weights.
    $form['content_ranking'] = array(
      '#type' => 'details',
      '#title' => t('Content ranking'),
598
      '#open' => TRUE,
599 600
    );
    $form['content_ranking']['info'] = array(
601
      '#markup' => '<p><em>' . $this->t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em></p>'
602
    );
603 604 605 606 607 608
    // Prepare table.
    $header = [$this->t('Factor'), $this->t('Influence')];
    $form['content_ranking']['rankings'] = array(
      '#type' => 'table',
      '#header' => $header,
    );
609 610

    // Note: reversed to reflect that higher number = higher ranking.
611 612
    $range = range(0, 10);
    $options = array_combine($range, $range);
613
    foreach ($this->getRankings() as $var => $values) {
614 615 616 617
      $form['content_ranking']['rankings'][$var]['name'] = array(
        '#markup' => $values['title'],
      );
      $form['content_ranking']['rankings'][$var]['value'] = array(
618 619
        '#type' => 'select',
        '#options' => $options,
620
        '#attributes' => ['aria-label' => $this->t("Influence of '@title'", ['@title' => $values['title']])],
621
        '#default_value' => isset($this->configuration['rankings'][$var]) ? $this->configuration['rankings'][$var] : 0,
622 623
      );
    }
624
    return $form;
625 626
  }

627 628 629
  /**
   * {@inheritdoc}
   */
630
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
631
    foreach ($this->getRankings() as $var => $values) {
632 633
      if (!$form_state->isValueEmpty(['rankings', $var, 'value'])) {
        $this->configuration['rankings'][$var] = $form_state->getValue(['rankings', $var, 'value']);
634 635 636
      }
      else {
        unset($this->configuration['rankings'][$var]);
637 638 639
      }
    }
  }
640

641
}