Autocompleter::addRecordToSource()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 13
rs 10
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|array
64
     */
65
    protected $customSearchField;
66
67
    /**
68
     * @var array
69
     */
70
    protected $customSearchCols;
71
72
    public function autocomplete(HTTPRequest $request): HTTPResponse
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',
103
            'Name',
104
            'Surname',
105
            'Email',
106
            'ID'
107
        ];
108
109
        // Ensure field exists, this is really rudimentary
110
        $db = $class::config()->db;
111
        foreach ($searchCandidates as $searchCandidate) {
112
            if ($searchField) {
113
                continue;
114
            }
115
            if (isset($db[$searchCandidate])) {
116
                $searchField = $searchCandidate;
117
            }
118
        }
119
120
        $searchCols = [$searchField];
121
122
        // For Surname, do something better
123
        if ($searchField == "Surname") {
124
            // Show first name, surname
125
            if (isset($db['FirstName'])) {
126
                $searchField = ['FirstName', 'Surname'];
127
                $searchCols = ['FirstName', 'Surname'];
128
            }
129
            // Also search email
130
            if (isset($db['Email'])) {
131
                $searchCols = ['FirstName', 'Surname', 'Email'];
132
            }
133
        }
134
135
136
        if ($this->customSearchField) {
137
            $searchField = $this->customSearchField;
138
        }
139
        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...
140
            $searchCols = $this->customSearchCols;
141
        }
142
143
        /** @var DataList $list */
144
        $list = $sng::get();
145
146
        // Make sure at least one field is not null...
147
        $where = [];
148
        foreach ($searchCols as $searchCol) {
149
            $where[] = $searchCol . ' IS NOT NULL';
150
        }
151
        $list = $list->whereAny($where);
152
        // ... and matches search term ...
153
        $where = [];
154
        foreach ($searchCols as $searchCol) {
155
            $where[$searchCol . ' LIKE ?'] = $term;
156
        }
157
        $list = $list->whereAny($where);
158
        // ... and any user set requirements
159
        $filters = $this->ajaxFilters;
160
        if (!empty($filters)) {
161
            $list = $list->filter($filters);
162
        }
163
        $where = $this->ajaxWhere;
164
        if (!empty($where)) {
165
            // Deal with in clause
166
            if (is_string($where)) {
167
                $customWhere = $where;
168
            } else {
169
                $customWhere = [];
170
                foreach ($where as $col => $param) {
171
                    // For array, we need a IN statement with a ? for each value
172
                    if (is_array($param)) {
173
                        $prepValue = [];
174
                        $params = [];
175
                        foreach ($param as $paramValue) {
176
                            $params[] = $paramValue;
177
                            $prepValue[] = "?";
178
                        }
179
                        $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
180
                    } else {
181
                        $customWhere["$col = ?"] = $param;
182
                    }
183
                }
184
            }
185
            $list = $list->where($customWhere);
186
        }
187
188
        $list = $list->limit($this->recordsLimit);
189
190
        $results = iterator_to_array($list);
191
        $data = [];
192
        foreach ($results as $record) {
193
            if (is_array($searchField)) {
194
                $labelParts = [];
195
                foreach ($searchField as $sf) {
196
                    $labelParts[] = $record->$sf ?? "";
197
                }
198
                $label = implode(" ", $labelParts);
199
            } else {
200
                $label = $record->$searchField ?? "";
201
            }
202
            $data[] = [
203
                $vars['valueField'] => $record->ID,
204
                $vars['labelField'] => $label ?? "(no label)",
205
            ];
206
        }
207
        $body = json_encode([$vars['dataKey'] => $data]);
208
209
        $response = new HTTPResponse($body);
210
        $response->addHeader('Content-Type', 'application/json');
211
        return $response;
212
    }
213
214
    /**
215
     * Add a record to the source
216
     *
217
     * Useful for ajax scenarios where the list is not prepopulated but still needs to display
218
     * something on first load
219
     *
220
     * @param DataObject $record
221
     * @return boolean true if the record has been added, false otherwise
222
     */
223
    public function addRecordToSource($record)
224
    {
225
        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...
226
            return false;
227
        }
228
        $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

228
        /** @scrutinizer ignore-call */ 
229
        $source = $this->getSource();
Loading history...
229
        // It's already in the source
230
        if (isset($source[$record->ID])) {
231
            return false;
232
        }
233
        $source[$record->ID] = $record->getTitle();
234
        $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

234
        $this->/** @scrutinizer ignore-call */ 
235
               setSource($source);
Loading history...
235
        return true;
236
    }
237
238
    /**
239
     * Method copied from MultiSelectField to make sure we call $this->setValue
240
     *
241
     * @param DataObject|DataObjectInterface $record
242
     */
243
    public function loadFromDataObject(DataObjectInterface $record)
244
    {
245
        $fieldName = $this->getName();
246
        if (empty($fieldName) || empty($record)) {
247
            return;
248
        }
249
250
        $relation = $record->hasMethod($fieldName)
251
            ? $record->$fieldName()
252
            : null;
253
254
        $isMulti = $this instanceof ListboxField;
255
256
        // Detect DB relation or field
257
        $value = null;
258
        if ($relation instanceof Relation) {
259
            // Load ids from relation
260
            $value = array_values($relation->getIDList() ?? []);
261
        } elseif ($record->hasField($fieldName)) {
262
            $str = $record->$fieldName;
263
            if ($str) {
264
                if (strpos($str, '[') === 0) {
265
                    $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

265
                    $value = json_decode($str, /** @scrutinizer ignore-type */ JSON_OBJECT_AS_ARRAY);
Loading history...
266
                } else {
267
                    $value = explode(',', $str);
268
                }
269
            }
270
        }
271
272
        if ($value) {
273
            if ($isMulti) {
274
                $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

274
                $this->/** @scrutinizer ignore-call */ 
275
                       setValue($value);
Loading history...
275
            } else {
276
                $this->setValue(array_pop($value));
277
            }
278
        }
279
    }
280
281
    /**
282
     * Return a link to this field.
283
     *
284
     * @param string $action
285
     * @return string
286
     */
287
    public function Link($action = null)
288
    {
289
        if ($this->form) {
290
            return Controller::join_links($this->form->FormAction(), 'field/' . $this->getName(), $action);
291
        }
292
        return Controller::join_links(Controller::curr()->Link(), 'field/' . $this->getName(), $action);
0 ignored issues
show
Bug introduced by
Are you sure the usage of SilverStripe\Control\Controller::curr()->Link() targeting SilverStripe\Control\RequestHandler::Link() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
293
    }
294
295
    abstract public function getAjax();
296
297
    abstract public function setAjax($url, $opts = []);
298
299
    abstract public function isAjax();
300
301
    /**
302
     * Define a callback that returns the results as a map of id => title
303
     *
304
     * @param string $class
305
     * @param array $filters
306
     * @param string|array $where
307
     * @return $this
308
     */
309
    public function setAjaxWizard($class, $filters = [], $where = null)
310
    {
311
        $this->ajaxClass = $class;
312
        $this->ajaxFilters = $filters;
313
        $this->ajaxWhere = $where;
314
        return $this;
315
    }
316
317
    /**
318
     * Get ajax where
319
     *
320
     * @return string|array
321
     */
322
    public function getAjaxWhere()
323
    {
324
        return $this->ajaxWhere;
325
    }
326
327
    /**
328
     * Set ajax where
329
     *
330
     * @param string|array $ajaxWhere
331
     * @return $this
332
     */
333
    public function setAjaxWhere($ajaxWhere)
334
    {
335
        $this->ajaxWhere = $ajaxWhere;
336
        return $this;
337
    }
338
339
    /**
340
     * Get ajax filters
341
     *
342
     * @return array
343
     */
344
    public function getAjaxFilters()
345
    {
346
        return $this->ajaxFilters;
347
    }
348
349
    /**
350
     * Set ajax filters
351
     *
352
     * @param array $ajaxFilters
353
     * @return $this
354
     */
355
    public function setAjaxFilters($ajaxFilters)
356
    {
357
        $this->ajaxFilters = $ajaxFilters;
358
        return $this;
359
    }
360
361
    /**
362
     * Get ajax class
363
     *
364
     * @return string
365
     */
366
    public function getAjaxClass()
367
    {
368
        return $this->ajaxClass;
369
    }
370
371
    /**
372
     * Set ajax class
373
     *
374
     * @param string $ajaxClass  Ajax class
375
     * @return $this
376
     */
377
    public function setAjaxClass(string $ajaxClass)
378
    {
379
        $this->ajaxClass = $ajaxClass;
380
381
        return $this;
382
    }
383
384
    /**
385
     * Get the value of customSearchField
386
     *
387
     * @return string|array
388
     */
389
    public function getCustomSearchField()
390
    {
391
        return $this->customSearchField;
392
    }
393
394
    /**
395
     * Set the value of customSearchField
396
     *
397
     * It must be a valid sql expression like CONCAT(FirstName,' ',Surname)
398
     *
399
     * This will be the label returned by the autocomplete
400
     *
401
     * @param string|array $customSearchField
402
     * @return $this
403
     */
404
    public function setCustomSearchField($customSearchField)
405
    {
406
        $this->customSearchField = $customSearchField;
407
        return $this;
408
    }
409
410
    /**
411
     * Get the value of customSearchCols
412
     *
413
     * @return array
414
     */
415
    public function getCustomSearchCols()
416
    {
417
        return $this->customSearchCols;
418
    }
419
420
    /**
421
     * Set the value of customSearchCols
422
     *
423
     * @param array $customSearchCols
424
     * @return $this
425
     */
426
    public function setCustomSearchCols(array $customSearchCols)
427
    {
428
        $this->customSearchCols = $customSearchCols;
429
        return $this;
430
    }
431
432
    /**
433
     * Get the value of recordsLimit
434
     * @return int
435
     */
436
    public function getRecordsLimit()
437
    {
438
        return $this->recordsLimit;
439
    }
440
441
    /**
442
     * Set the value of recordsLimit
443
     *
444
     * @param int $recordsLimit
445
     */
446
    public function setRecordsLimit($recordsLimit)
447
    {
448
        $this->recordsLimit = $recordsLimit;
449
        return $this;
450
    }
451
452
    /**
453
     * Get the value of ajaxFullSearch
454
     * @return bool
455
     */
456
    public function getAjaxFullSearch()
457
    {
458
        return $this->ajaxFullSearch;
459
    }
460
461
    /**
462
     * Set the value of ajaxWildcard
463
     *
464
     * @param boolean $ajaxWildcard
465
     */
466
    public function setAjaxFullSearch($ajaxFullSearch)
467
    {
468
        $this->ajaxFullSearch = $ajaxFullSearch;
469
        return $this;
470
    }
471
}
472