InOperator.php 14.5 KB
Newer Older
merlinofchaos's avatar
merlinofchaos committed
1 2 3 4
<?php

/**
 * @file
5
 * Definition of Drupal\views\Plugin\views\filter\InOperator.
merlinofchaos's avatar
merlinofchaos committed
6 7
 */

8
namespace Drupal\views\Plugin\views\filter;
9

10
use Drupal\Component\Utility\String as UtilityString;
11
use Drupal\Component\Utility\Unicode;
12
use Drupal\Core\Form\FormStateInterface;
13
use Drupal\views\Plugin\views\display\DisplayPluginBase;
14
use Drupal\views\ViewExecutable;
15
use Drupal\Core\Form\OptGroup;
dawehner's avatar
dawehner committed
16

merlinofchaos's avatar
merlinofchaos committed
17 18 19 20 21 22 23 24
/**
 * Simple filter to handle matching of multiple options selectable via checkboxes
 *
 * Definition items:
 * - options callback: The function to call in order to generate the value options. If omitted, the options 'Yes' and 'No' will be used.
 * - options arguments: An array of arguments to pass to the options callback.
 *
 * @ingroup views_filter_handlers
25
 *
26
 * @ViewsFilter("in_operator")
dawehner's avatar
dawehner committed
27
 */
28
class InOperator extends FilterPluginBase {
29

30
  protected $valueFormType = 'checkboxes';
merlinofchaos's avatar
merlinofchaos committed
31 32 33 34 35

  /**
   * @var array
   * Stores all operations which are available on the form.
   */
36 37 38 39 40 41 42 43
  protected $valueOptions = NULL;

  /**
   * The filter title.
   *
   * @var string
   */
  protected $valueTitle;
merlinofchaos's avatar
merlinofchaos committed
44

45
  /**
46
   * Overrides \Drupal\views\Plugin\views\filter\FilterPluginBase::init().
47
   */
48 49
  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
    parent::init($view, $display, $options);
50

51 52
    $this->valueTitle = $this->t('Options');
    $this->valueOptions = NULL;
merlinofchaos's avatar
merlinofchaos committed
53 54 55 56 57 58 59 60 61 62 63
  }

  /**
   * Child classes should be used to override this function and set the
   * 'value options', unless 'options callback' is defined as a valid function
   * or static public method to generate these values.
   *
   * This can use a guard to be used to reduce database hits as much as
   * possible.
   *
   * @return
64
   *   Return the stored values in $this->valueOptions if someone expects it.
merlinofchaos's avatar
merlinofchaos committed
65
   */
66
  public function getValueOptions() {
67
    if (isset($this->valueOptions)) {
merlinofchaos's avatar
merlinofchaos committed
68 69 70 71 72
      return;
    }

    if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) {
      if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) {
73
        $this->valueOptions = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']);
merlinofchaos's avatar
merlinofchaos committed
74 75
      }
      else {
76
        $this->valueOptions = call_user_func($this->definition['options callback']);
merlinofchaos's avatar
merlinofchaos committed
77 78 79
      }
    }
    else {
80
      $this->valueOptions = array(t('Yes'), $this->t('No'));
merlinofchaos's avatar
merlinofchaos committed
81 82
    }

83
    return $this->valueOptions;
merlinofchaos's avatar
merlinofchaos committed
84 85
  }

86 87
  public function defaultExposeOptions() {
    parent::defaultExposeOptions();
merlinofchaos's avatar
merlinofchaos committed
88 89 90
    $this->options['expose']['reduce'] = FALSE;
  }

91
  public function buildExposeForm(&$form, FormStateInterface $form_state) {
92
    parent::buildExposeForm($form, $form_state);
merlinofchaos's avatar
merlinofchaos committed
93 94
    $form['expose']['reduce'] = array(
      '#type' => 'checkbox',
95 96
      '#title' => $this->t('Limit list to selected items'),
      '#description' => $this->t('If checked, the only items presented to the user will be the ones selected here.'),
merlinofchaos's avatar
merlinofchaos committed
97 98 99 100
      '#default_value' => !empty($this->options['expose']['reduce']), // safety
    );
  }

101 102
  protected function defineOptions() {
    $options = parent::defineOptions();
merlinofchaos's avatar
merlinofchaos committed
103 104 105

    $options['operator']['default'] = 'in';
    $options['value']['default'] = array();
106
    $options['expose']['contains']['reduce'] = array('default' => FALSE);
merlinofchaos's avatar
merlinofchaos committed
107 108 109 110 111 112 113 114 115 116 117 118

    return $options;
  }

  /**
   * This kind of construct makes it relatively easy for a child class
   * to add or remove functionality by overriding this function and
   * adding/removing items from this array.
   */
  function operators() {
    $operators = array(
      'in' => array(
119 120 121
        'title' => $this->t('Is one of'),
        'short' => $this->t('in'),
        'short_single' => $this->t('='),
122
        'method' => 'opSimple',
merlinofchaos's avatar
merlinofchaos committed
123 124 125
        'values' => 1,
      ),
      'not in' => array(
126 127 128
        'title' => $this->t('Is not one of'),
        'short' => $this->t('not in'),
        'short_single' => $this->t('<>'),
129
        'method' => 'opSimple',
merlinofchaos's avatar
merlinofchaos committed
130 131 132 133 134 135 136
        'values' => 1,
      ),
    );
    // if the definition allows for the empty operator, add it.
    if (!empty($this->definition['allow empty'])) {
      $operators += array(
        'empty' => array(
137
          'title' => $this->t('Is empty (NULL)'),
138
          'method' => 'opEmpty',
139
          'short' => $this->t('empty'),
merlinofchaos's avatar
merlinofchaos committed
140 141 142
          'values' => 0,
        ),
        'not empty' => array(
143
          'title' => $this->t('Is not empty (NOT NULL)'),
144
          'method' => 'opEmpty',
145
          'short' => $this->t('not empty'),
merlinofchaos's avatar
merlinofchaos committed
146 147 148 149 150 151 152 153 154 155 156
          'values' => 0,
        ),
      );
    }

    return $operators;
  }

  /**
   * Build strings from the operators() for 'select' options
   */
157
  public function operatorOptions($which = 'title') {
merlinofchaos's avatar
merlinofchaos committed
158 159 160 161 162 163 164 165
    $options = array();
    foreach ($this->operators() as $id => $info) {
      $options[$id] = $info[$which];
    }

    return $options;
  }

166
  protected function operatorValues($values = 1) {
merlinofchaos's avatar
merlinofchaos committed
167 168 169 170 171 172 173 174 175 176
    $options = array();
    foreach ($this->operators() as $id => $info) {
      if (isset($info['values']) && $info['values'] == $values) {
        $options[] = $id;
      }
    }

    return $options;
  }

177
  protected function valueForm(&$form, FormStateInterface $form_state) {
merlinofchaos's avatar
merlinofchaos committed
178 179 180
    $form['value'] = array();
    $options = array();

181 182
    $exposed = $form_state->get('exposed');
    if (!$exposed) {
merlinofchaos's avatar
merlinofchaos committed
183
      // Add a select all option to the value form.
184
      $options = array('all' => $this->t('Select all'));
merlinofchaos's avatar
merlinofchaos committed
185 186
    }

187
    $this->getValueOptions();
188
    $options += $this->valueOptions;
merlinofchaos's avatar
merlinofchaos committed
189 190 191 192
    $default_value = (array) $this->value;

    $which = 'all';
    if (!empty($form['operator'])) {
193
      $source = ':input[name="options[operator]"]';
merlinofchaos's avatar
merlinofchaos committed
194
    }
195
    if ($exposed) {
merlinofchaos's avatar
merlinofchaos committed
196 197 198 199
      $identifier = $this->options['expose']['identifier'];

      if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
        // exposed and locked.
200
        $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
merlinofchaos's avatar
merlinofchaos committed
201 202
      }
      else {
203
        $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
merlinofchaos's avatar
merlinofchaos committed
204 205 206
      }

      if (!empty($this->options['expose']['reduce'])) {
207
        $options = $this->reduceValueOptions();
merlinofchaos's avatar
merlinofchaos committed
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

        if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
          $default_value = array();
        }
      }

      if (empty($this->options['expose']['multiple'])) {
        if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
          $default_value = 'All';
        }
        elseif (empty($default_value)) {
          $keys = array_keys($options);
          $default_value = array_shift($keys);
        }
        else {
          $copy = $default_value;
          $default_value = array_shift($copy);
        }
      }
    }

    if ($which == 'all' || $which == 'value') {
      $form['value'] = array(
231
        '#type' => $this->valueFormType,
232
        '#title' => $this->valueTitle,
merlinofchaos's avatar
merlinofchaos committed
233 234 235 236 237 238
        '#options' => $options,
        '#default_value' => $default_value,
        // These are only valid for 'select' type, but do no harm to checkboxes.
        '#multiple' => TRUE,
        '#size' => count($options) > 8 ? 8 : count($options),
      );
239
      $user_input = $form_state->getUserInput();
240
      if ($exposed && !isset($user_input[$identifier])) {
241 242
        $user_input[$identifier] = $default_value;
        $form_state->setUserInput($user_input);
merlinofchaos's avatar
merlinofchaos committed
243 244 245
      }

      if ($which == 'all') {
246
        if (!$exposed && (in_array($this->valueFormType, ['checkbox', 'checkboxes', 'radios', 'select']))) {
merlinofchaos's avatar
merlinofchaos committed
247 248 249
          $form['value']['#prefix'] = '<div id="edit-options-value-wrapper">';
          $form['value']['#suffix'] = '</div>';
        }
250
        // Setup #states for all operators with one value.
251
        foreach ($this->operatorValues(1) as $operator) {
252 253 254 255
          $form['value']['#states']['visible'][] = array(
            $source => array('value' => $operator),
          );
        }
merlinofchaos's avatar
merlinofchaos committed
256 257 258 259 260 261 262
      }
    }
  }

  /**
   * When using exposed filters, we may be required to reduce the set.
   */
263
  public function reduceValueOptions($input = NULL) {
merlinofchaos's avatar
merlinofchaos committed
264
    if (!isset($input)) {
265
      $input = $this->valueOptions;
merlinofchaos's avatar
merlinofchaos committed
266 267 268 269 270 271 272 273
    }

    // Because options may be an array of strings, or an array of mixed arrays
    // and strings (optgroups) or an array of objects, we have to
    // step through and handle each one individually.
    $options = array();
    foreach ($input as $id => $option) {
      if (is_array($option)) {
274
        $options[$id] = $this->reduceValueOptions($option);
merlinofchaos's avatar
merlinofchaos committed
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
        continue;
      }
      elseif (is_object($option)) {
        $keys = array_keys($option->option);
        $key = array_shift($keys);
        if (isset($this->options['value'][$key])) {
          $options[$id] = $option;
        }
      }
      elseif (isset($this->options['value'][$id])) {
        $options[$id] = $option;
      }
    }
    return $options;
  }

291
  public function acceptExposedInput($input) {
merlinofchaos's avatar
merlinofchaos committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    // A very special override because the All state for this type of
    // filter could have a default:
    if (empty($this->options['exposed'])) {
      return TRUE;
    }

    // If this is non-multiple and non-required, then this filter will
    // participate, but using the default settings, *if* 'limit is true.
    if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) {
      $identifier = $this->options['expose']['identifier'];
      if ($input[$identifier] == 'All') {
        return TRUE;
      }
    }

307
    return parent::acceptExposedInput($input);
merlinofchaos's avatar
merlinofchaos committed
308 309
  }

310
  protected function valueSubmit($form, FormStateInterface $form_state) {
merlinofchaos's avatar
merlinofchaos committed
311 312 313 314 315 316 317 318 319
    // Drupal's FAPI system automatically puts '0' in for any checkbox that
    // was not set, and the key to the checkbox if it is set.
    // Unfortunately, this means that if the key to that checkbox is 0,
    // we are unable to tell if that checkbox was set or not.

    // Luckily, the '#value' on the checkboxes form actually contains
    // *only* a list of checkboxes that were set, and we can use that
    // instead.

320
    $form_state->setValue(array('options', 'value'), $form['value']['#value']);
merlinofchaos's avatar
merlinofchaos committed
321 322
  }

323 324
  public function adminSummary() {
    if ($this->isAGroup()) {
325
      return $this->t('grouped');
326
    }
merlinofchaos's avatar
merlinofchaos committed
327
    if (!empty($this->options['exposed'])) {
328
      return $this->t('exposed');
merlinofchaos's avatar
merlinofchaos committed
329 330 331
    }
    $info = $this->operators();

332
    $this->getValueOptions();
merlinofchaos's avatar
merlinofchaos committed
333 334 335 336 337

    if (!is_array($this->value)) {
      return;
    }

338
    $operator = UtilityString::checkPlain($info[$this->operator]['short']);
merlinofchaos's avatar
merlinofchaos committed
339
    $values = '';
340
    if (in_array($this->operator, $this->operatorValues(1))) {
merlinofchaos's avatar
merlinofchaos committed
341 342
      // Remove every element which is not known.
      foreach ($this->value as $value) {
343
        if (!isset($this->valueOptions[$value])) {
merlinofchaos's avatar
merlinofchaos committed
344 345 346 347 348
          unset($this->value[$value]);
        }
      }
      // Choose different kind of ouput for 0, a single and multiple values.
      if (count($this->value) == 0) {
349
        $values = $this->t('Unknown');
merlinofchaos's avatar
merlinofchaos committed
350 351 352 353
      }
      else if (count($this->value) == 1) {
        // If any, use the 'single' short name of the operator instead.
        if (isset($info[$this->operator]['short_single'])) {
354
          $operator = UtilityString::checkPlain($info[$this->operator]['short_single']);
merlinofchaos's avatar
merlinofchaos committed
355 356 357 358
        }

        $keys = $this->value;
        $value = array_shift($keys);
359 360
        if (isset($this->valueOptions[$value])) {
          $values = UtilityString::checkPlain($this->valueOptions[$value]);
merlinofchaos's avatar
merlinofchaos committed
361 362 363 364 365 366 367 368 369 370
        }
        else {
          $values = '';
        }
      }
      else {
        foreach ($this->value as $value) {
          if ($values !== '') {
            $values .= ', ';
          }
371
          if (Unicode::strlen($values) > 8) {
372
            $values = Unicode::truncate($values, 8, FALSE, TRUE);
merlinofchaos's avatar
merlinofchaos committed
373 374
            break;
          }
375 376
          if (isset($this->valueOptions[$value])) {
            $values .= UtilityString::checkPlain($this->valueOptions[$value]);
merlinofchaos's avatar
merlinofchaos committed
377 378 379 380 381 382 383 384
          }
        }
      }
    }

    return $operator . (($values !== '') ? ' ' . $values : '');
  }

385
  public function query() {
merlinofchaos's avatar
merlinofchaos committed
386 387 388 389 390 391
    $info = $this->operators();
    if (!empty($info[$this->operator]['method'])) {
      $this->{$info[$this->operator]['method']}();
    }
  }

392
  protected function opSimple() {
merlinofchaos's avatar
merlinofchaos committed
393 394 395
    if (empty($this->value)) {
      return;
    }
396
    $this->ensureMyTable();
merlinofchaos's avatar
merlinofchaos committed
397 398 399

    // We use array_values() because the checkboxes keep keys and that can cause
    // array addition problems.
400
    $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", array_values($this->value), $this->operator);
merlinofchaos's avatar
merlinofchaos committed
401 402
  }

403
  protected function opEmpty() {
404
    $this->ensureMyTable();
merlinofchaos's avatar
merlinofchaos committed
405 406 407 408 409 410 411
    if ($this->operator == 'empty') {
      $operator = "IS NULL";
    }
    else {
      $operator = "IS NOT NULL";
    }

412
    $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", NULL, $operator);
merlinofchaos's avatar
merlinofchaos committed
413 414
  }

415
  public function validate() {
416
    $this->getValueOptions();
merlinofchaos's avatar
merlinofchaos committed
417 418 419 420
    $errors = array();

    // If the operator is an operator which doesn't require a value, there is
    // no need for additional validation.
421
    if (in_array($this->operator, $this->operatorValues(0))) {
merlinofchaos's avatar
merlinofchaos committed
422 423 424
      return array();
    }

425
    if (!in_array($this->operator, $this->operatorValues(1))) {
426
      $errors[] = $this->t('The operator is invalid on filter: @filter.', array('@filter' => $this->adminLabel(TRUE)));
merlinofchaos's avatar
merlinofchaos committed
427 428
    }
    if (is_array($this->value)) {
429
      if (!isset($this->valueOptions)) {
merlinofchaos's avatar
merlinofchaos committed
430 431 432 433 434 435 436 437 438
        // Don't validate if there are none value options provided, for example for special handlers.
        return $errors;
      }
      if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) {
        // Don't validate if the field is exposed and no default value is provided.
        return $errors;
      }

      // Some filter_in_operator usage uses optgroups forms, so flatten it.
439
      $flat_options = OptGroup::flattenOptions($this->valueOptions);
merlinofchaos's avatar
merlinofchaos committed
440 441 442 443 444 445 446 447 448

      // Remove every element which is not known.
      foreach ($this->value as $value) {
        if (!isset($flat_options[$value])) {
          unset($this->value[$value]);
        }
      }
      // Choose different kind of ouput for 0, a single and multiple values.
      if (count($this->value) == 0) {
449
        $errors[] = $this->t('No valid values found on filter: @filter.', array('@filter' => $this->adminLabel(TRUE)));
merlinofchaos's avatar
merlinofchaos committed
450 451 452
      }
    }
    elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) {
453
      $errors[] = $this->t('The value @value is not an array for @operator on filter: @filter', array('@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE)));
merlinofchaos's avatar
merlinofchaos committed
454 455 456
    }
    return $errors;
  }
457

merlinofchaos's avatar
merlinofchaos committed
458
}