Version 1
[yaffs-website] / web / core / modules / views / src / Plugin / views / filter / StringFilter.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\filter;
4
5 use Drupal\Core\Form\FormStateInterface;
6
7 /**
8  * Basic textfield filter to handle string filtering commands
9  * including equality, like, not like, etc.
10  *
11  * @ingroup views_filter_handlers
12  *
13  * @ViewsFilter("string")
14  */
15 class StringFilter extends FilterPluginBase {
16
17   /**
18    * All words separated by spaces or sentences encapsulated by double quotes.
19    */
20   const WORDS_PATTERN = '/ (-?)("[^"]+"|[^" ]+)/i';
21
22   // exposed filter options
23   protected $alwaysMultiple = TRUE;
24
25   protected function defineOptions() {
26     $options = parent::defineOptions();
27
28     $options['expose']['contains']['required'] = ['default' => FALSE];
29
30     return $options;
31   }
32
33   /**
34    * This kind of construct makes it relatively easy for a child class
35    * to add or remove functionality by overriding this function and
36    * adding/removing items from this array.
37    */
38   public function operators() {
39     $operators = [
40       '=' => [
41         'title' => $this->t('Is equal to'),
42         'short' => $this->t('='),
43         'method' => 'opEqual',
44         'values' => 1,
45       ],
46       '!=' => [
47         'title' => $this->t('Is not equal to'),
48         'short' => $this->t('!='),
49         'method' => 'opEqual',
50         'values' => 1,
51       ],
52       'contains' => [
53         'title' => $this->t('Contains'),
54         'short' => $this->t('contains'),
55         'method' => 'opContains',
56         'values' => 1,
57       ],
58       'word' => [
59         'title' => $this->t('Contains any word'),
60         'short' => $this->t('has word'),
61         'method' => 'opContainsWord',
62         'values' => 1,
63       ],
64       'allwords' => [
65         'title' => $this->t('Contains all words'),
66         'short' => $this->t('has all'),
67         'method' => 'opContainsWord',
68         'values' => 1,
69       ],
70       'starts' => [
71         'title' => $this->t('Starts with'),
72         'short' => $this->t('begins'),
73         'method' => 'opStartsWith',
74         'values' => 1,
75       ],
76       'not_starts' => [
77         'title' => $this->t('Does not start with'),
78         'short' => $this->t('not_begins'),
79         'method' => 'opNotStartsWith',
80         'values' => 1,
81       ],
82       'ends' => [
83         'title' => $this->t('Ends with'),
84         'short' => $this->t('ends'),
85         'method' => 'opEndsWith',
86         'values' => 1,
87       ],
88       'not_ends' => [
89         'title' => $this->t('Does not end with'),
90         'short' => $this->t('not_ends'),
91         'method' => 'opNotEndsWith',
92         'values' => 1,
93       ],
94       'not' => [
95         'title' => $this->t('Does not contain'),
96         'short' => $this->t('!has'),
97         'method' => 'opNotLike',
98         'values' => 1,
99       ],
100       'shorterthan' => [
101         'title' => $this->t('Length is shorter than'),
102         'short' => $this->t('shorter than'),
103         'method' => 'opShorterThan',
104         'values' => 1,
105       ],
106       'longerthan' => [
107         'title' => $this->t('Length is longer than'),
108         'short' => $this->t('longer than'),
109         'method' => 'opLongerThan',
110         'values' => 1,
111       ],
112       'regular_expression' => [
113         'title' => $this->t('Regular expression'),
114         'short' => $this->t('regex'),
115         'method' => 'opRegex',
116         'values' => 1,
117       ],
118     ];
119     // if the definition allows for the empty operator, add it.
120     if (!empty($this->definition['allow empty'])) {
121       $operators += [
122         'empty' => [
123           'title' => $this->t('Is empty (NULL)'),
124           'method' => 'opEmpty',
125           'short' => $this->t('empty'),
126           'values' => 0,
127         ],
128         'not empty' => [
129           'title' => $this->t('Is not empty (NOT NULL)'),
130           'method' => 'opEmpty',
131           'short' => $this->t('not empty'),
132           'values' => 0,
133         ],
134       ];
135     }
136
137     return $operators;
138   }
139
140   /**
141    * Build strings from the operators() for 'select' options
142    */
143   public function operatorOptions($which = 'title') {
144     $options = [];
145     foreach ($this->operators() as $id => $info) {
146       $options[$id] = $info[$which];
147     }
148
149     return $options;
150   }
151
152   public function adminSummary() {
153     if ($this->isAGroup()) {
154       return $this->t('grouped');
155     }
156     if (!empty($this->options['exposed'])) {
157       return $this->t('exposed');
158     }
159
160     $options = $this->operatorOptions('short');
161     $output = '';
162     if (!empty($options[$this->operator])) {
163       $output = $options[$this->operator];
164     }
165     if (in_array($this->operator, $this->operatorValues(1))) {
166       $output .= ' ' . $this->value;
167     }
168     return $output;
169   }
170
171   protected function operatorValues($values = 1) {
172     $options = [];
173     foreach ($this->operators() as $id => $info) {
174       if (isset($info['values']) && $info['values'] == $values) {
175         $options[] = $id;
176       }
177     }
178
179     return $options;
180   }
181
182   /**
183    * Provide a simple textfield for equality
184    */
185   protected function valueForm(&$form, FormStateInterface $form_state) {
186     // We have to make some choices when creating this as an exposed
187     // filter form. For example, if the operator is locked and thus
188     // not rendered, we can't render dependencies; instead we only
189     // render the form items we need.
190     $which = 'all';
191     if (!empty($form['operator'])) {
192       $source = ':input[name="options[operator]"]';
193     }
194     if ($exposed = $form_state->get('exposed')) {
195       $identifier = $this->options['expose']['identifier'];
196
197       if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
198         // exposed and locked.
199         $which = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
200       }
201       else {
202         $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
203       }
204     }
205
206     if ($which == 'all' || $which == 'value') {
207       $form['value'] = [
208         '#type' => 'textfield',
209         '#title' => $this->t('Value'),
210         '#size' => 30,
211         '#default_value' => $this->value,
212       ];
213       $user_input = $form_state->getUserInput();
214       if ($exposed && !isset($user_input[$identifier])) {
215         $user_input[$identifier] = $this->value;
216         $form_state->setUserInput($user_input);
217       }
218
219       if ($which == 'all') {
220         // Setup #states for all operators with one value.
221         foreach ($this->operatorValues(1) as $operator) {
222           $form['value']['#states']['visible'][] = [
223             $source => ['value' => $operator],
224           ];
225         }
226       }
227     }
228
229     if (!isset($form['value'])) {
230       // Ensure there is something in the 'value'.
231       $form['value'] = [
232         '#type' => 'value',
233         '#value' => NULL
234       ];
235     }
236   }
237
238   public function operator() {
239     return $this->operator == '=' ? 'LIKE' : 'NOT LIKE';
240   }
241
242   /**
243    * Add this filter to the query.
244    *
245    * Due to the nature of fapi, the value and the operator have an unintended
246    * level of indirection. You will find them in $this->operator
247    * and $this->value respectively.
248    */
249   public function query() {
250     $this->ensureMyTable();
251     $field = "$this->tableAlias.$this->realField";
252
253     $info = $this->operators();
254     if (!empty($info[$this->operator]['method'])) {
255       $this->{$info[$this->operator]['method']}($field);
256     }
257   }
258
259   public function opEqual($field) {
260     $this->query->addWhere($this->options['group'], $field, $this->value, $this->operator());
261   }
262
263   protected function opContains($field) {
264     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value) . '%', 'LIKE');
265   }
266
267   protected function opContainsWord($field) {
268     $where = $this->operator == 'word' ? db_or() : db_and();
269
270     // Don't filter on empty strings.
271     if (empty($this->value)) {
272       return;
273     }
274
275     preg_match_all(static::WORDS_PATTERN, ' ' . $this->value, $matches, PREG_SET_ORDER);
276     foreach ($matches as $match) {
277       $phrase = FALSE;
278       // Strip off phrase quotes
279       if ($match[2]{0} == '"') {
280         $match[2] = substr($match[2], 1, -1);
281         $phrase = TRUE;
282       }
283       $words = trim($match[2], ',?!();:-');
284       $words = $phrase ? [$words] : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
285       foreach ($words as $word) {
286         $where->condition($field, '%' . db_like(trim($word, " ,!?")) . '%', 'LIKE');
287       }
288     }
289
290     if ($where->count() === 0) {
291       return;
292     }
293
294     // previously this was a call_user_func_array but that's unnecessary
295     // as views will unpack an array that is a single arg.
296     $this->query->addWhere($this->options['group'], $where);
297   }
298
299   protected function opStartsWith($field) {
300     $this->query->addWhere($this->options['group'], $field, db_like($this->value) . '%', 'LIKE');
301   }
302
303   protected function opNotStartsWith($field) {
304     $this->query->addWhere($this->options['group'], $field, db_like($this->value) . '%', 'NOT LIKE');
305   }
306
307   protected function opEndsWith($field) {
308     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value), 'LIKE');
309   }
310
311   protected function opNotEndsWith($field) {
312     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value), 'NOT LIKE');
313   }
314
315   protected function opNotLike($field) {
316     $this->query->addWhere($this->options['group'], $field, '%' . db_like($this->value) . '%', 'NOT LIKE');
317   }
318
319   protected function opShorterThan($field) {
320     $placeholder = $this->placeholder();
321     // Type cast the argument to an integer because the SQLite database driver
322     // has to do some specific alterations to the query base on that data type.
323     $this->query->addWhereExpression($this->options['group'], "LENGTH($field) < $placeholder", [$placeholder => (int) $this->value]);
324   }
325
326   protected function opLongerThan($field) {
327     $placeholder = $this->placeholder();
328     // Type cast the argument to an integer because the SQLite database driver
329     // has to do some specific alterations to the query base on that data type.
330     $this->query->addWhereExpression($this->options['group'], "LENGTH($field) > $placeholder", [$placeholder => (int) $this->value]);
331   }
332
333   /**
334    * Filters by a regular expression.
335    *
336    * @param string $field
337    *   The expression pointing to the queries field, for example "foo.bar".
338    */
339   protected function opRegex($field) {
340     $this->query->addWhere($this->options['group'], $field, $this->value, 'REGEXP');
341   }
342
343   protected function opEmpty($field) {
344     if ($this->operator == 'empty') {
345       $operator = "IS NULL";
346     }
347     else {
348       $operator = "IS NOT NULL";
349     }
350
351     $this->query->addWhere($this->options['group'], $field, NULL, $operator);
352   }
353
354 }