Passed
Push — master ( 880cb8...69dc82 )
by Thomas
02:29
created

Autocompleter   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 436
Duplicated Lines 0 %

Importance

Changes 4
Bugs 3 Features 0
Metric Value
eloc 147
dl 0
loc 436
rs 6.4799
c 4
b 3
f 0
wmc 54

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getAjaxClass() 0 3 1
A setAjaxClass() 0 5 1
A setAjaxFullSearch() 0 4 1
A Link() 0 3 1
A getAjaxFullSearch() 0 3 1
A setAjaxWizard() 0 6 1
F autocomplete() 0 136 24
A setCustomSearchCols() 0 4 1
A getRecordsLimit() 0 3 1
A getAjaxWhere() 0 3 1
A setAjaxFilters() 0 4 1
B loadFromDataObject() 0 34 10
A getCustomSearchCols() 0 3 1
A setRecordsLimit() 0 4 1
A getAjaxFilters() 0 3 1
A setCustomSearchField() 0 4 1
A getCustomSearchField() 0 3 1
A addRecordToSource() 0 13 4
A setAjaxWhere() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Autocompleter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Autocompleter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LeKoala\FormElements;
4
5
use SilverStripe\ORM\DataList;
6
use SilverStripe\ORM\Relation;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Forms\ListboxField;
12
use SilverStripe\ORM\DataObjectInterface;
13
14
/**
15
 * Provide most of the implementation for the AjaxPoweredField
16
 * You need to implement yourself:
17
 * - getAjax
18
 * - setAjax
19
 * - isAjax
20
 */
21
trait Autocompleter
22
{
23
    /**
24
     * @config
25
     * @var array
26
     */
27
    private static $allowed_actions = [
28
        'autocomplete'
29
    ];
30
31
    /**
32
     * Ajax class
33
     *
34
     * @var string
35
     */
36
    protected $ajaxClass;
37
38
    /**
39
     * Ajax where
40
     *
41
     * @var string|array
42
     */
43
    protected $ajaxWhere;
44
45
    /**
46
     * Ajax filters
47
     *
48
     * @var array
49
     */
50
    protected $ajaxFilters = [];
51
52
    /**
53
     * @var boolean
54
     */
55
    protected $ajaxFullSearch = false;
56
57
    /**
58
     * @var int
59
     */
60
    protected $recordsLimit = 1000;
61
62
    /**
63
     * @var string
64
     */
65
    protected $customSearchField;
66
67
    /**
68
     * @var array
69
     */
70
    protected $customSearchCols;
71
72
    public function autocomplete(HTTPRequest $request)
73
    {
74
        if ($this->isDisabled() || $this->isReadonly()) {
0 ignored issues
show
Bug introduced by
It seems like isReadonly() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

74
        if ($this->isDisabled() || $this->/** @scrutinizer ignore-call */ isReadonly()) {
Loading history...
Bug introduced by
It seems like isDisabled() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

74
        if ($this->/** @scrutinizer ignore-call */ isDisabled() || $this->isReadonly()) {
Loading history...
75
            return $this->httpError(403);
0 ignored issues
show
Bug introduced by
It seems like httpError() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

75
            return $this->/** @scrutinizer ignore-call */ httpError(403);
Loading history...
76
        }
77
78
        // CSRF check
79
        $token = $this->getForm()->getSecurityToken();
0 ignored issues
show
Bug introduced by
It seems like getForm() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

79
        $token = $this->/** @scrutinizer ignore-call */ getForm()->getSecurityToken();
Loading history...
80
        if (!$token->checkRequest($request)) {
81
            return $this->httpError(400, "Invalid token");
82
        }
83
84
        $name = $this->getName();
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
Bug introduced by
It seems like getName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

84
        /** @scrutinizer ignore-call */ 
85
        $name = $this->getName();
Loading history...
85
86
        $vars = $this->getServerVars();
0 ignored issues
show
Bug introduced by
It seems like getServerVars() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

86
        /** @scrutinizer ignore-call */ 
87
        $vars = $this->getServerVars();
Loading history...
87
88
        // Don't use by default % at the start of term as it prevents use of indexes
89
        $term = $request->getVar($vars['queryParam']) . '%';
90
        $term = str_replace(' ', '%', $term);
91
        if ($this->ajaxFullSearch) {
92
            $term = "%" . $term;
93
        }
94
95
        $class = $this->ajaxClass;
96
97
        $sng = $class::singleton();
98
        $baseTable = $sng->baseTable();
0 ignored issues
show
Unused Code introduced by
The assignment to $baseTable is dead and can be removed.
Loading history...
99
100
        $searchField = '';
101
        $searchCandidates = [
102
            'Title', 'Name', 'Surname', 'Email', 'ID'
103
        ];
104
105
        // Ensure field exists, this is really rudimentary
106
        $db = $class::config()->db;
107
        foreach ($searchCandidates as $searchCandidate) {
108
            if ($searchField) {
109
                continue;
110
            }
111
            if (isset($db[$searchCandidate])) {
112
                $searchField = $searchCandidate;
113
            }
114
        }
115
116
        $searchCols = [$searchField];
117
118
        // For Surname, do something better
119
        if ($searchField == "Surname") {
120
            // Show first name, surname
121
            if (isset($db['FirstName'])) {
122
                $searchField = ['FirstName', 'Surname'];
123
                $searchCols = ['FirstName', 'Surname'];
124
            }
125
            // Also search email
126
            if (isset($db['Email'])) {
127
                $searchCols = ['FirstName', 'Surname', 'Email'];
128
            }
129
        }
130
131
132
        if ($this->customSearchField) {
133
            $searchField = $this->customSearchField;
134
        }
135
        if ($this->customSearchCols) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customSearchCols of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
136
            $searchCols = $this->customSearchCols;
137
        }
138
139
        /** @var DataList $list */
140
        $list = $sng::get();
141
142
        // Make sure at least one field is not null...
143
        $where = [];
144
        foreach ($searchCols as $searchCol) {
145
            $where[] = $searchCol . ' IS NOT NULL';
146
        }
147
        $list = $list->whereAny($where);
148
        // ... and matches search term ...
149
        $where = [];
150
        foreach ($searchCols as $searchCol) {
151
            $where[$searchCol . ' LIKE ?'] = $term;
152
        }
153
        $list = $list->whereAny($where);
154
        // ... and any user set requirements
155
        $filters = $this->ajaxFilters;
156
        if (!empty($filters)) {
157
            $list = $list->filter($filters);
158
        }
159
        $where = $this->ajaxWhere;
160
        if (!empty($where)) {
161
            // Deal with in clause
162
            if (is_string($where)) {
163
                $customWhere = $where;
164
            } else {
165
                $customWhere = [];
166
                foreach ($where as $col => $param) {
167
                    // For array, we need a IN statement with a ? for each value
168
                    if (is_array($param)) {
169
                        $prepValue = [];
170
                        $params = [];
171
                        foreach ($param as $paramValue) {
172
                            $params[] = $paramValue;
173
                            $prepValue[] = "?";
174
                        }
175
                        $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
176
                    } else {
177
                        $customWhere["$col = ?"] = $param;
178
                    }
179
                }
180
            }
181
            $list = $list->where($customWhere);
182
        }
183
184
        $list = $list->limit($this->recordsLimit);
185
186
        $results = iterator_to_array($list);
187
        $data = [];
188
        foreach ($results as $record) {
189
            if (is_array($searchField)) {
190
                $labelParts = [];
191
                foreach ($searchField as $sf) {
192
                    $labelParts[] = $record->$sf ?? "";
193
                }
194
                $label = implode(" ", $labelParts);
195
            } else {
196
                $label = $record->$searchField ?? "";
197
            }
198
            $data[] = [
199
                $vars['valueField'] => $record->ID,
200
                $vars['labelField'] => $label ?? "(no label)",
201
            ];
202
        }
203
        $body = json_encode([$vars['dataKey'] => $data]);
204
205
        $response = new HTTPResponse($body);
206
        $response->addHeader('Content-Type', 'application/json');
207
        return $response;
208
    }
209
210
    /**
211
     * Add a record to the source
212
     *
213
     * Useful for ajax scenarios where the list is not prepopulated but still needs to display
214
     * something on first load
215
     *
216
     * @param DataObject $record
217
     * @return boolean true if the record has been added, false otherwise
218
     */
219
    public function addRecordToSource($record)
220
    {
221
        if (!$record || !$this->isAjax()) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
Bug introduced by
It seems like isAjax() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
        if (!$record || !$this->/** @scrutinizer ignore-call */ isAjax()) {
Loading history...
222
            return false;
223
        }
224
        $source = $this->getSource();
0 ignored issues
show
Bug introduced by
It seems like getSource() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

224
        /** @scrutinizer ignore-call */ 
225
        $source = $this->getSource();
Loading history...
225
        // It's already in the source
226
        if (isset($source[$record->ID])) {
227
            return false;
228
        }
229
        $source[$record->ID] = $record->getTitle();
230
        $this->setSource($source);
0 ignored issues
show
Bug introduced by
It seems like setSource() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

230
        $this->/** @scrutinizer ignore-call */ 
231
               setSource($source);
Loading history...
231
        return true;
232
    }
233
234
    /**
235
     * Method copied from MultiSelectField to make sure we call $this->setValue
236
     *
237
     * @param DataObject|DataObjectInterface $record
238
     */
239
    public function loadFromDataObject(DataObjectInterface $record)
240
    {
241
        $fieldName = $this->getName();
242
        if (empty($fieldName) || empty($record)) {
243
            return;
244
        }
245
246
        $relation = $record->hasMethod($fieldName)
247
            ? $record->$fieldName()
248
            : null;
249
250
        $isMulti = $this instanceof ListboxField;
251
252
        // Detect DB relation or field
253
        $value = null;
254
        if ($relation instanceof Relation) {
255
            // Load ids from relation
256
            $value = array_values($relation->getIDList() ?? []);
257
        } elseif ($record->hasField($fieldName)) {
258
            $str = $record->$fieldName;
259
            if ($str) {
260
                if (strpos($str, '[') === 0) {
261
                    $value = json_decode($str, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Bug introduced by
LeKoala\FormElements\JSON_OBJECT_AS_ARRAY of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

261
                    $value = json_decode($str, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
262
                } else {
263
                    $value = explode(',', $str);
264
                }
265
            }
266
        }
267
268
        if ($value) {
269
            if ($isMulti) {
270
                $this->setValue($value);
0 ignored issues
show
Bug introduced by
It seems like setValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

270
                $this->/** @scrutinizer ignore-call */ 
271
                       setValue($value);
Loading history...
271
            } else {
272
                $this->setValue(array_pop($value));
273
            }
274
        }
275
    }
276
277
    /**
278
     * Return a link to this field.
279
     *
280
     * @param string $action
281
     * @return string
282
     */
283
    public function Link($action = null)
284
    {
285
        return Controller::join_links($this->form->FormAction(), 'field/' . $this->getName(), $action);
286
    }
287
288
    /**
289
     * Define a callback that returns the results as a map of id => title
290
     *
291
     * @param string $class
292
     * @param array $filters
293
     * @param string|array $where
294
     * @return $this
295
     */
296
    public function setAjaxWizard($class, $filters = [], $where = null)
297
    {
298
        $this->ajaxClass = $class;
299
        $this->ajaxFilters = $filters;
300
        $this->ajaxWhere = $where;
301
        return $this;
302
    }
303
304
    /**
305
     * Get ajax where
306
     *
307
     * @return string
308
     */
309
    public function getAjaxWhere()
310
    {
311
        return $this->ajaxWhere;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->ajaxWhere also could return the type array which is incompatible with the documented return type string.
Loading history...
312
    }
313
314
    /**
315
     * Set ajax where
316
     *
317
     * @param string $ajaxWhere
318
     * @return $this
319
     */
320
    public function setAjaxWhere($ajaxWhere)
321
    {
322
        $this->ajaxWhere = $ajaxWhere;
323
        return $this;
324
    }
325
326
    /**
327
     * Get ajax filters
328
     *
329
     * @return string
330
     */
331
    public function getAjaxFilters()
332
    {
333
        return $this->ajaxFilters;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->ajaxFilters returns the type array which is incompatible with the documented return type string.
Loading history...
334
    }
335
336
    /**
337
     * Set ajax filters
338
     *
339
     * @param string $ajaxFilters
340
     * @return $this
341
     */
342
    public function setAjaxFilters($ajaxFilters)
343
    {
344
        $this->ajaxFilters = $ajaxFilters;
0 ignored issues
show
Documentation Bug introduced by
It seems like $ajaxFilters of type string is incompatible with the declared type array of property $ajaxFilters.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
345
        return $this;
346
    }
347
348
    /**
349
     * Get ajax class
350
     *
351
     * @return string
352
     */
353
    public function getAjaxClass()
354
    {
355
        return $this->ajaxClass;
356
    }
357
358
    /**
359
     * Set ajax class
360
     *
361
     * @param string $ajaxClass  Ajax class
362
     * @return $this
363
     */
364
    public function setAjaxClass(string $ajaxClass)
365
    {
366
        $this->ajaxClass = $ajaxClass;
367
368
        return $this;
369
    }
370
371
    /**
372
     * Get the value of customSearchField
373
     *
374
     * @return string
375
     */
376
    public function getCustomSearchField(): string
377
    {
378
        return $this->customSearchField;
379
    }
380
381
    /**
382
     * Set the value of customSearchField
383
     *
384
     * It must be a valid sql expression like CONCAT(FirstName,' ',Surname)
385
     *
386
     * This will be the label returned by the autocomplete
387
     *
388
     * @param string $customSearchField
389
     * @return $this
390
     */
391
    public function setCustomSearchField(string $customSearchField)
392
    {
393
        $this->customSearchField = $customSearchField;
394
        return $this;
395
    }
396
397
    /**
398
     * Get the value of customSearchCols
399
     *
400
     * @return array
401
     */
402
    public function getCustomSearchCols()
403
    {
404
        return $this->customSearchCols;
405
    }
406
407
    /**
408
     * Set the value of customSearchCols
409
     *
410
     * @param array $customSearchCols
411
     * @return $this
412
     */
413
    public function setCustomSearchCols(array $customSearchCols)
414
    {
415
        $this->customSearchCols = $customSearchCols;
416
        return $this;
417
    }
418
419
    /**
420
     * Get the value of recordsLimit
421
     * @return int
422
     */
423
    public function getRecordsLimit()
424
    {
425
        return $this->recordsLimit;
426
    }
427
428
    /**
429
     * Set the value of recordsLimit
430
     *
431
     * @param int $recordsLimit
432
     */
433
    public function setRecordsLimit($recordsLimit)
434
    {
435
        $this->recordsLimit = $recordsLimit;
436
        return $this;
437
    }
438
439
    /**
440
     * Get the value of ajaxFullSearch
441
     * @return bool
442
     */
443
    public function getAjaxFullSearch()
444
    {
445
        return $this->ajaxFullSearch;
446
    }
447
448
    /**
449
     * Set the value of ajaxWildcard
450
     *
451
     * @param boolean $ajaxWildcard
452
     */
453
    public function setAjaxFullSearch($ajaxFullSearch)
454
    {
455
        $this->ajaxFullSearch = $ajaxFullSearch;
456
        return $this;
457
    }
458
}
459