e0add0fcb1bb8871a9439bae65c2b1b106f85e7e
[yaffs-website] / web / core / modules / views / src / Plugin / views / filter / InOperator.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\filter;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\views\Plugin\views\display\DisplayPluginBase;
8 use Drupal\views\ViewExecutable;
9 use Drupal\Core\Form\OptGroup;
10
11 /**
12  * Simple filter to handle matching of multiple options selectable via checkboxes
13  *
14  * Definition items:
15  * - options callback: The function to call in order to generate the value options. If omitted, the options 'Yes' and 'No' will be used.
16  * - options arguments: An array of arguments to pass to the options callback.
17  *
18  * @ingroup views_filter_handlers
19  *
20  * @ViewsFilter("in_operator")
21  */
22 class InOperator extends FilterPluginBase {
23
24   protected $valueFormType = 'checkboxes';
25
26   /**
27    * @var array
28    * Stores all operations which are available on the form.
29    */
30   protected $valueOptions = NULL;
31
32   /**
33    * The filter title.
34    *
35    * @var string
36    */
37   protected $valueTitle;
38
39   /**
40    * {@inheritdoc}
41    */
42   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
43     parent::init($view, $display, $options);
44
45     $this->valueTitle = $this->t('Options');
46     $this->valueOptions = NULL;
47   }
48
49   /**
50    * Child classes should be used to override this function and set the
51    * 'value options', unless 'options callback' is defined as a valid function
52    * or static public method to generate these values.
53    *
54    * This can use a guard to be used to reduce database hits as much as
55    * possible.
56    *
57    * @return array|null
58    *   The stored values from $this->valueOptions.
59    */
60   public function getValueOptions() {
61     if (isset($this->valueOptions)) {
62       return $this->valueOptions;
63     }
64
65     if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) {
66       if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) {
67         $this->valueOptions = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']);
68       }
69       else {
70         $this->valueOptions = call_user_func($this->definition['options callback']);
71       }
72     }
73     else {
74       $this->valueOptions = [t('Yes'), $this->t('No')];
75     }
76
77     return $this->valueOptions;
78   }
79
80   public function defaultExposeOptions() {
81     parent::defaultExposeOptions();
82     $this->options['expose']['reduce'] = FALSE;
83   }
84
85   public function buildExposeForm(&$form, FormStateInterface $form_state) {
86     parent::buildExposeForm($form, $form_state);
87     $form['expose']['reduce'] = [
88       '#type' => 'checkbox',
89       '#title' => $this->t('Limit list to selected items'),
90       '#description' => $this->t('If checked, the only items presented to the user will be the ones selected here.'),
91       // Safety.
92       '#default_value' => !empty($this->options['expose']['reduce']),
93     ];
94   }
95
96   protected function defineOptions() {
97     $options = parent::defineOptions();
98
99     $options['operator']['default'] = 'in';
100     $options['value']['default'] = [];
101     $options['expose']['contains']['reduce'] = ['default' => FALSE];
102
103     return $options;
104   }
105
106   /**
107    * This kind of construct makes it relatively easy for a child class
108    * to add or remove functionality by overriding this function and
109    * adding/removing items from this array.
110    */
111   public function operators() {
112     $operators = [
113       'in' => [
114         'title' => $this->t('Is one of'),
115         'short' => $this->t('in'),
116         'short_single' => $this->t('='),
117         'method' => 'opSimple',
118         'values' => 1,
119       ],
120       'not in' => [
121         'title' => $this->t('Is not one of'),
122         'short' => $this->t('not in'),
123         'short_single' => $this->t('<>'),
124         'method' => 'opSimple',
125         'values' => 1,
126       ],
127     ];
128     // if the definition allows for the empty operator, add it.
129     if (!empty($this->definition['allow empty'])) {
130       $operators += [
131         'empty' => [
132           'title' => $this->t('Is empty (NULL)'),
133           'method' => 'opEmpty',
134           'short' => $this->t('empty'),
135           'values' => 0,
136         ],
137         'not empty' => [
138           'title' => $this->t('Is not empty (NOT NULL)'),
139           'method' => 'opEmpty',
140           'short' => $this->t('not empty'),
141           'values' => 0,
142         ],
143       ];
144     }
145
146     return $operators;
147   }
148
149   /**
150    * Build strings from the operators() for 'select' options
151    */
152   public function operatorOptions($which = 'title') {
153     $options = [];
154     foreach ($this->operators() as $id => $info) {
155       $options[$id] = $info[$which];
156     }
157
158     return $options;
159   }
160
161   protected function operatorValues($values = 1) {
162     $options = [];
163     foreach ($this->operators() as $id => $info) {
164       if (isset($info['values']) && $info['values'] == $values) {
165         $options[] = $id;
166       }
167     }
168
169     return $options;
170   }
171
172   protected function valueForm(&$form, FormStateInterface $form_state) {
173     $form['value'] = [];
174     $options = [];
175
176     $exposed = $form_state->get('exposed');
177     if (!$exposed) {
178       // Add a select all option to the value form.
179       $options = ['all' => $this->t('Select all')];
180     }
181
182     $this->getValueOptions();
183     $options += $this->valueOptions;
184     $default_value = (array) $this->value;
185
186     $which = 'all';
187     if (!empty($form['operator'])) {
188       $source = ':input[name="options[operator]"]';
189     }
190     if ($exposed) {
191       $identifier = $this->options['expose']['identifier'];
192
193       if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
194         // exposed and locked.
195         $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
196       }
197       else {
198         $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
199       }
200
201       if (!empty($this->options['expose']['reduce'])) {
202         $options = $this->reduceValueOptions();
203
204         if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
205           $default_value = [];
206         }
207       }
208
209       if (empty($this->options['expose']['multiple'])) {
210         if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce'])) || isset($this->options['value']['all'])) {
211           $default_value = 'All';
212         }
213         elseif (empty($default_value)) {
214           $keys = array_keys($options);
215           $default_value = array_shift($keys);
216         }
217         else {
218           $copy = $default_value;
219           $default_value = array_shift($copy);
220         }
221       }
222     }
223
224     if ($which == 'all' || $which == 'value') {
225       $form['value'] = [
226         '#type' => $this->valueFormType,
227         '#title' => $this->valueTitle,
228         '#options' => $options,
229         '#default_value' => $default_value,
230         // These are only valid for 'select' type, but do no harm to checkboxes.
231         '#multiple' => TRUE,
232         // The value options can be a multidimensional array if the value form
233         // type is a select list, so make sure that they are counted correctly.
234         '#size' => min(count($options, COUNT_RECURSIVE), 8),
235       ];
236       $user_input = $form_state->getUserInput();
237       if ($exposed && !isset($user_input[$identifier])) {
238         $user_input[$identifier] = $default_value;
239         $form_state->setUserInput($user_input);
240       }
241
242       if ($which == 'all') {
243         if (!$exposed && (in_array($this->valueFormType, ['checkbox', 'checkboxes', 'radios', 'select']))) {
244           $form['value']['#prefix'] = '<div id="edit-options-value-wrapper">';
245           $form['value']['#suffix'] = '</div>';
246         }
247         // Setup #states for all operators with one value.
248         foreach ($this->operatorValues(1) as $operator) {
249           $form['value']['#states']['visible'][] = [
250             $source => ['value' => $operator],
251           ];
252         }
253       }
254     }
255   }
256
257   /**
258    * When using exposed filters, we may be required to reduce the set.
259    */
260   public function reduceValueOptions($input = NULL) {
261     if (!isset($input)) {
262       $input = $this->valueOptions;
263     }
264
265     // Because options may be an array of strings, or an array of mixed arrays
266     // and strings (optgroups) or an array of objects, we have to
267     // step through and handle each one individually.
268     $options = [];
269     foreach ($input as $id => $option) {
270       if (is_array($option)) {
271         $options[$id] = $this->reduceValueOptions($option);
272         continue;
273       }
274       elseif (is_object($option)) {
275         $keys = array_keys($option->option);
276         $key = array_shift($keys);
277         if (isset($this->options['value'][$key])) {
278           $options[$id] = $option;
279         }
280       }
281       elseif (isset($this->options['value'][$id])) {
282         $options[$id] = $option;
283       }
284     }
285     return $options;
286   }
287
288   /**
289    * {@inheritdoc}
290    */
291   public function acceptExposedInput($input) {
292     if (empty($this->options['exposed'])) {
293       return TRUE;
294     }
295
296     // The "All" state for this type of filter could have a default value. If
297     // this is a non-multiple and non-required option, then this filter will
298     // participate by using the default settings *if* 'limit' is true.
299     if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) {
300       $identifier = $this->options['expose']['identifier'];
301       if ($input[$identifier] == 'All') {
302         return TRUE;
303       }
304     }
305
306     return parent::acceptExposedInput($input);
307   }
308
309   protected function valueSubmit($form, FormStateInterface $form_state) {
310     // Drupal's FAPI system automatically puts '0' in for any checkbox that
311     // was not set, and the key to the checkbox if it is set.
312     // Unfortunately, this means that if the key to that checkbox is 0,
313     // we are unable to tell if that checkbox was set or not.
314
315     // Luckily, the '#value' on the checkboxes form actually contains
316     // *only* a list of checkboxes that were set, and we can use that
317     // instead.
318
319     $form_state->setValue(['options', 'value'], $form['value']['#value']);
320   }
321
322   public function adminSummary() {
323     if ($this->isAGroup()) {
324       return $this->t('grouped');
325     }
326     if (!empty($this->options['exposed'])) {
327       return $this->t('exposed');
328     }
329     $info = $this->operators();
330
331     $this->getValueOptions();
332     // Some filter_in_operator usage uses optgroups forms, so flatten it.
333     $flat_options = OptGroup::flattenOptions($this->valueOptions);
334
335     if (!is_array($this->value)) {
336       return;
337     }
338
339     $operator = $info[$this->operator]['short'];
340     $values = '';
341     if (in_array($this->operator, $this->operatorValues(1))) {
342       // Remove every element which is not known.
343       foreach ($this->value as $value) {
344         if (!isset($flat_options[$value])) {
345           unset($this->value[$value]);
346         }
347       }
348       // Choose different kind of output for 0, a single and multiple values.
349       if (count($this->value) == 0) {
350         $values = $this->t('Unknown');
351       }
352       elseif (count($this->value) == 1) {
353         // If any, use the 'single' short name of the operator instead.
354         if (isset($info[$this->operator]['short_single'])) {
355           $operator = $info[$this->operator]['short_single'];
356         }
357
358         $keys = $this->value;
359         $value = array_shift($keys);
360         if (isset($flat_options[$value])) {
361           $values = $flat_options[$value];
362         }
363         else {
364           $values = '';
365         }
366       }
367       else {
368         foreach ($this->value as $value) {
369           if ($values !== '') {
370             $values .= ', ';
371           }
372           if (mb_strlen($values) > 8) {
373             $values = Unicode::truncate($values, 8, FALSE, TRUE);
374             break;
375           }
376           if (isset($flat_options[$value])) {
377             $values .= $flat_options[$value];
378           }
379         }
380       }
381     }
382
383     return $operator . (($values !== '') ? ' ' . $values : '');
384   }
385
386   public function query() {
387     $info = $this->operators();
388     if (!empty($info[$this->operator]['method'])) {
389       $this->{$info[$this->operator]['method']}();
390     }
391   }
392
393   protected function opSimple() {
394     if (empty($this->value)) {
395       return;
396     }
397     $this->ensureMyTable();
398
399     // We use array_values() because the checkboxes keep keys and that can cause
400     // array addition problems.
401     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", array_values($this->value), $this->operator);
402   }
403
404   protected function opEmpty() {
405     $this->ensureMyTable();
406     if ($this->operator == 'empty') {
407       $operator = "IS NULL";
408     }
409     else {
410       $operator = "IS NOT NULL";
411     }
412
413     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", NULL, $operator);
414   }
415
416   public function validate() {
417     $this->getValueOptions();
418     $errors = parent::validate();
419
420     // If the operator is an operator which doesn't require a value, there is
421     // no need for additional validation.
422     if (in_array($this->operator, $this->operatorValues(0))) {
423       return [];
424     }
425
426     if (!in_array($this->operator, $this->operatorValues(1))) {
427       $errors[] = $this->t('The operator is invalid on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
428     }
429     if (is_array($this->value)) {
430       if (!isset($this->valueOptions)) {
431         // Don't validate if there are none value options provided, for example for special handlers.
432         return $errors;
433       }
434       if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) {
435         // Don't validate if the field is exposed and no default value is provided.
436         return $errors;
437       }
438
439       // Some filter_in_operator usage uses optgroups forms, so flatten it.
440       $flat_options = OptGroup::flattenOptions($this->valueOptions);
441
442       // Remove every element which is not known.
443       foreach ($this->value as $value) {
444         if (!isset($flat_options[$value])) {
445           unset($this->value[$value]);
446         }
447       }
448       // Choose different kind of output for 0, a single and multiple values.
449       if (count($this->value) == 0) {
450         $errors[] = $this->t('No valid values found on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
451       }
452     }
453     elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) {
454       $errors[] = $this->t('The value @value is not an array for @operator on filter: @filter', ['@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE)]);
455     }
456     return $errors;
457   }
458
459 }