Version 1
[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       '#default_value' => !empty($this->options['expose']['reduce']), // safety
92     ];
93   }
94
95   protected function defineOptions() {
96     $options = parent::defineOptions();
97
98     $options['operator']['default'] = 'in';
99     $options['value']['default'] = [];
100     $options['expose']['contains']['reduce'] = ['default' => FALSE];
101
102     return $options;
103   }
104
105   /**
106    * This kind of construct makes it relatively easy for a child class
107    * to add or remove functionality by overriding this function and
108    * adding/removing items from this array.
109    */
110   public function operators() {
111     $operators = [
112       'in' => [
113         'title' => $this->t('Is one of'),
114         'short' => $this->t('in'),
115         'short_single' => $this->t('='),
116         'method' => 'opSimple',
117         'values' => 1,
118       ],
119       'not in' => [
120         'title' => $this->t('Is not one of'),
121         'short' => $this->t('not in'),
122         'short_single' => $this->t('<>'),
123         'method' => 'opSimple',
124         'values' => 1,
125       ],
126     ];
127     // if the definition allows for the empty operator, add it.
128     if (!empty($this->definition['allow empty'])) {
129       $operators += [
130         'empty' => [
131           'title' => $this->t('Is empty (NULL)'),
132           'method' => 'opEmpty',
133           'short' => $this->t('empty'),
134           'values' => 0,
135         ],
136         'not empty' => [
137           'title' => $this->t('Is not empty (NOT NULL)'),
138           'method' => 'opEmpty',
139           'short' => $this->t('not empty'),
140           'values' => 0,
141         ],
142       ];
143     }
144
145     return $operators;
146   }
147
148   /**
149    * Build strings from the operators() for 'select' options
150    */
151   public function operatorOptions($which = 'title') {
152     $options = [];
153     foreach ($this->operators() as $id => $info) {
154       $options[$id] = $info[$which];
155     }
156
157     return $options;
158   }
159
160   protected function operatorValues($values = 1) {
161     $options = [];
162     foreach ($this->operators() as $id => $info) {
163       if (isset($info['values']) && $info['values'] == $values) {
164         $options[] = $id;
165       }
166     }
167
168     return $options;
169   }
170
171   protected function valueForm(&$form, FormStateInterface $form_state) {
172     $form['value'] = [];
173     $options = [];
174
175     $exposed = $form_state->get('exposed');
176     if (!$exposed) {
177       // Add a select all option to the value form.
178       $options = ['all' => $this->t('Select all')];
179     }
180
181     $this->getValueOptions();
182     $options += $this->valueOptions;
183     $default_value = (array) $this->value;
184
185     $which = 'all';
186     if (!empty($form['operator'])) {
187       $source = ':input[name="options[operator]"]';
188     }
189     if ($exposed) {
190       $identifier = $this->options['expose']['identifier'];
191
192       if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
193         // exposed and locked.
194         $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
195       }
196       else {
197         $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
198       }
199
200       if (!empty($this->options['expose']['reduce'])) {
201         $options = $this->reduceValueOptions();
202
203         if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
204           $default_value = [];
205         }
206       }
207
208       if (empty($this->options['expose']['multiple'])) {
209         if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce'])) || isset($this->options['value']['all'])) {
210           $default_value = 'All';
211         }
212         elseif (empty($default_value)) {
213           $keys = array_keys($options);
214           $default_value = array_shift($keys);
215         }
216         else {
217           $copy = $default_value;
218           $default_value = array_shift($copy);
219         }
220       }
221     }
222
223     if ($which == 'all' || $which == 'value') {
224       $form['value'] = [
225         '#type' => $this->valueFormType,
226         '#title' => $this->valueTitle,
227         '#options' => $options,
228         '#default_value' => $default_value,
229         // These are only valid for 'select' type, but do no harm to checkboxes.
230         '#multiple' => TRUE,
231         '#size' => count($options) > 8 ? 8 : count($options),
232       ];
233       $user_input = $form_state->getUserInput();
234       if ($exposed && !isset($user_input[$identifier])) {
235         $user_input[$identifier] = $default_value;
236         $form_state->setUserInput($user_input);
237       }
238
239       if ($which == 'all') {
240         if (!$exposed && (in_array($this->valueFormType, ['checkbox', 'checkboxes', 'radios', 'select']))) {
241           $form['value']['#prefix'] = '<div id="edit-options-value-wrapper">';
242           $form['value']['#suffix'] = '</div>';
243         }
244         // Setup #states for all operators with one value.
245         foreach ($this->operatorValues(1) as $operator) {
246           $form['value']['#states']['visible'][] = [
247             $source => ['value' => $operator],
248           ];
249         }
250       }
251     }
252   }
253
254   /**
255    * When using exposed filters, we may be required to reduce the set.
256    */
257   public function reduceValueOptions($input = NULL) {
258     if (!isset($input)) {
259       $input = $this->valueOptions;
260     }
261
262     // Because options may be an array of strings, or an array of mixed arrays
263     // and strings (optgroups) or an array of objects, we have to
264     // step through and handle each one individually.
265     $options = [];
266     foreach ($input as $id => $option) {
267       if (is_array($option)) {
268         $options[$id] = $this->reduceValueOptions($option);
269         continue;
270       }
271       elseif (is_object($option)) {
272         $keys = array_keys($option->option);
273         $key = array_shift($keys);
274         if (isset($this->options['value'][$key])) {
275           $options[$id] = $option;
276         }
277       }
278       elseif (isset($this->options['value'][$id])) {
279         $options[$id] = $option;
280       }
281     }
282     return $options;
283   }
284
285   /**
286    * {@inheritdoc}
287    */
288   public function acceptExposedInput($input) {
289     if (empty($this->options['exposed'])) {
290       return TRUE;
291     }
292
293     // The "All" state for this type of filter could have a default value. If
294     // this is a non-multiple and non-required option, then this filter will
295     // participate by using the default settings *if* 'limit' is true.
296     if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) {
297       $identifier = $this->options['expose']['identifier'];
298       if ($input[$identifier] == 'All') {
299         return TRUE;
300       }
301     }
302
303     return parent::acceptExposedInput($input);
304   }
305
306   protected function valueSubmit($form, FormStateInterface $form_state) {
307     // Drupal's FAPI system automatically puts '0' in for any checkbox that
308     // was not set, and the key to the checkbox if it is set.
309     // Unfortunately, this means that if the key to that checkbox is 0,
310     // we are unable to tell if that checkbox was set or not.
311
312     // Luckily, the '#value' on the checkboxes form actually contains
313     // *only* a list of checkboxes that were set, and we can use that
314     // instead.
315
316     $form_state->setValue(['options', 'value'], $form['value']['#value']);
317   }
318
319   public function adminSummary() {
320     if ($this->isAGroup()) {
321       return $this->t('grouped');
322     }
323     if (!empty($this->options['exposed'])) {
324       return $this->t('exposed');
325     }
326     $info = $this->operators();
327
328     $this->getValueOptions();
329     // Some filter_in_operator usage uses optgroups forms, so flatten it.
330     $flat_options = OptGroup::flattenOptions($this->valueOptions);
331
332     if (!is_array($this->value)) {
333       return;
334     }
335
336     $operator = $info[$this->operator]['short'];
337     $values = '';
338     if (in_array($this->operator, $this->operatorValues(1))) {
339       // Remove every element which is not known.
340       foreach ($this->value as $value) {
341         if (!isset($flat_options[$value])) {
342           unset($this->value[$value]);
343         }
344       }
345       // Choose different kind of output for 0, a single and multiple values.
346       if (count($this->value) == 0) {
347         $values = $this->t('Unknown');
348       }
349       elseif (count($this->value) == 1) {
350         // If any, use the 'single' short name of the operator instead.
351         if (isset($info[$this->operator]['short_single'])) {
352           $operator = $info[$this->operator]['short_single'];
353         }
354
355         $keys = $this->value;
356         $value = array_shift($keys);
357         if (isset($flat_options[$value])) {
358           $values = $flat_options[$value];
359         }
360         else {
361           $values = '';
362         }
363       }
364       else {
365         foreach ($this->value as $value) {
366           if ($values !== '') {
367             $values .= ', ';
368           }
369           if (Unicode::strlen($values) > 8) {
370             $values = Unicode::truncate($values, 8, FALSE, TRUE);
371             break;
372           }
373           if (isset($flat_options[$value])) {
374             $values .= $flat_options[$value];
375           }
376         }
377       }
378     }
379
380     return $operator . (($values !== '') ? ' ' . $values : '');
381   }
382
383   public function query() {
384     $info = $this->operators();
385     if (!empty($info[$this->operator]['method'])) {
386       $this->{$info[$this->operator]['method']}();
387     }
388   }
389
390   protected function opSimple() {
391     if (empty($this->value)) {
392       return;
393     }
394     $this->ensureMyTable();
395
396     // We use array_values() because the checkboxes keep keys and that can cause
397     // array addition problems.
398     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", array_values($this->value), $this->operator);
399   }
400
401   protected function opEmpty() {
402     $this->ensureMyTable();
403     if ($this->operator == 'empty') {
404       $operator = "IS NULL";
405     }
406     else {
407       $operator = "IS NOT NULL";
408     }
409
410     $this->query->addWhere($this->options['group'], "$this->tableAlias.$this->realField", NULL, $operator);
411   }
412
413   public function validate() {
414     $this->getValueOptions();
415     $errors = parent::validate();
416
417     // If the operator is an operator which doesn't require a value, there is
418     // no need for additional validation.
419     if (in_array($this->operator, $this->operatorValues(0))) {
420       return [];
421     }
422
423     if (!in_array($this->operator, $this->operatorValues(1))) {
424       $errors[] = $this->t('The operator is invalid on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
425     }
426     if (is_array($this->value)) {
427       if (!isset($this->valueOptions)) {
428         // Don't validate if there are none value options provided, for example for special handlers.
429         return $errors;
430       }
431       if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) {
432         // Don't validate if the field is exposed and no default value is provided.
433         return $errors;
434       }
435
436       // Some filter_in_operator usage uses optgroups forms, so flatten it.
437       $flat_options = OptGroup::flattenOptions($this->valueOptions);
438
439       // Remove every element which is not known.
440       foreach ($this->value as $value) {
441         if (!isset($flat_options[$value])) {
442           unset($this->value[$value]);
443         }
444       }
445       // Choose different kind of output for 0, a single and multiple values.
446       if (count($this->value) == 0) {
447         $errors[] = $this->t('No valid values found on filter: @filter.', ['@filter' => $this->adminLabel(TRUE)]);
448       }
449     }
450     elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) {
451       $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)]);
452     }
453     return $errors;
454   }
455
456 }