Passed
Pull Request — 4 (#9991)
by Thomas
16:46
created

scaffoldSearchFields()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 25
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 43
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Control\HTTPResponse;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Forms\FieldList;
11
use SilverStripe\Forms\TextField;
12
use SilverStripe\ORM\SS_List;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DataList;
15
use SilverStripe\View\ArrayData;
16
use SilverStripe\View\SSViewer;
17
use LogicException;
18
use SilverStripe\ORM\Filters\SearchFilter;
19
20
/**
21
 * This class is is responsible for adding objects to another object's has_many
22
 * and many_many relation, as defined by the {@link RelationList} passed to the
23
 * {@link GridField} constructor.
24
 *
25
 * Objects can be searched through an input field (partially matching one or
26
 * more fields).
27
 *
28
 * Selecting from the results will add the object to the relation.
29
 *
30
 * Often used alongside {@link GridFieldDeleteAction} for detaching existing
31
 * records from a relationship.
32
 *
33
 * For easier setup, have a look at a sample configuration in
34
 * {@link GridFieldConfig_RelationEditor}.
35
 */
36
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
37
{
38
39
    /**
40
     * The HTML fragment to write this component into
41
     */
42
    protected $targetFragment;
43
44
    /**
45
     * @var SS_List
46
     */
47
    protected $searchList;
48
49
    /**
50
     * Define column names which should be included in the search.
51
     * By default, they're searched with a {@link StartsWithFilter}.
52
     * To define custom filters, use the same notation as {@link DataList->filter()},
53
     * e.g. "Name:EndsWith".
54
     *
55
     * If multiple fields are provided, the filtering is performed non-exclusive.
56
     * If no fields are provided, tries to auto-detect fields from
57
     * {@link DataObject->searchableFields()}.
58
     *
59
     * The fields support "dot-notation" for relationships, e.g.
60
     * a entry called "Team.Name" will search through the names of
61
     * a "Team" relationship.
62
     *
63
     * @example
64
     *  array(
65
     *      'Name',
66
     *      'Email:StartsWith',
67
     *      'Team.Name'
68
     *  )
69
     *
70
     * @var array
71
     */
72
    protected $searchFields = [];
73
74
    /**
75
     * @var string SSViewer template to render the results presentation
76
     */
77
    protected $resultsFormat = '$Title';
78
79
    /**
80
     * @var string Text shown on the search field, instructing what to search for.
81
     */
82
    protected $placeholderText;
83
84
    /**
85
     * @var int
86
     */
87
    protected $resultsLimit = 20;
88
89
    /**
90
     *
91
     * @param string $targetFragment
92
     * @param array $searchFields Which fields on the object in the list should be searched
93
     */
94
    public function __construct($targetFragment = 'before', $searchFields = null)
95
    {
96
        $this->targetFragment = $targetFragment;
97
        $this->searchFields = (array)$searchFields;
98
    }
99
100
    /**
101
     *
102
     * @param GridField $gridField
103
     * @return string[] - HTML
104
     */
105
    public function getHTMLFragments($gridField)
106
    {
107
        $dataClass = $gridField->getModelClass();
108
109
        $forTemplate = new ArrayData([]);
110
        $forTemplate->Fields = new FieldList();
0 ignored issues
show
Bug Best Practice introduced by
The property Fields does not exist on SilverStripe\View\ArrayData. Since you implemented __set, consider adding a @property annotation.
Loading history...
111
112
        $searchField = new TextField('gridfield_relationsearch', _t('SilverStripe\\Forms\\GridField\\GridField.RelationSearch', "Relation search"));
113
114
        $searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
115
        $searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
116
        $searchField->addExtraClass('relation-search no-change-track action_gridfield_relationsearch');
117
118
        $findAction = new GridField_FormAction(
119
            $gridField,
120
            'gridfield_relationfind',
121
            _t('SilverStripe\\Forms\\GridField\\GridField.Find', "Find"),
122
            'find',
123
            'find'
0 ignored issues
show
Bug introduced by
'find' of type string is incompatible with the type array expected by parameter $args of SilverStripe\Forms\GridF...rmAction::__construct(). ( Ignorable by Annotation )

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

123
            /** @scrutinizer ignore-type */ 'find'
Loading history...
124
        );
125
        $findAction->setAttribute('data-icon', 'relationfind');
126
        $findAction->addExtraClass('action_gridfield_relationfind');
127
128
        $addAction = new GridField_FormAction(
129
            $gridField,
130
            'gridfield_relationadd',
131
            _t('SilverStripe\\Forms\\GridField\\GridField.LinkExisting', "Link Existing"),
132
            'addto',
133
            'addto'
134
        );
135
        $addAction->setAttribute('data-icon', 'chain--plus');
136
        $addAction->addExtraClass('btn btn-outline-secondary font-icon-link action_gridfield_relationadd');
137
138
        // If an object is not found, disable the action
139
        if (!is_int($gridField->State->GridFieldAddRelation(null))) {
0 ignored issues
show
Bug introduced by
The method GridFieldAddRelation() does not exist on SilverStripe\Forms\GridField\GridState_Data. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

139
        if (!is_int($gridField->State->/** @scrutinizer ignore-call */ GridFieldAddRelation(null))) {
Loading history...
140
            $addAction->setDisabled(true);
141
        }
142
143
        $forTemplate->Fields->push($searchField);
0 ignored issues
show
Bug Best Practice introduced by
The property Fields does not exist on SilverStripe\View\ArrayData. Since you implemented __get, consider adding a @property annotation.
Loading history...
144
        $forTemplate->Fields->push($findAction);
145
        $forTemplate->Fields->push($addAction);
146
        if ($form = $gridField->getForm()) {
147
            $forTemplate->Fields->setForm($form);
148
        }
149
150
        $template = SSViewer::get_templates_by_class($this, '', __CLASS__);
151
        return [
152
            $this->targetFragment => $forTemplate->renderWith($template)
153
        ];
154
    }
155
156
    /**
157
     *
158
     * @param GridField $gridField
159
     * @return array
160
     */
161
    public function getActions($gridField)
162
    {
163
        return ['addto', 'find'];
164
    }
165
166
    /**
167
     * Manipulate the state to add a new relation
168
     *
169
     * @param GridField $gridField
170
     * @param string $actionName Action identifier, see {@link getActions()}.
171
     * @param array $arguments Arguments relevant for this
172
     * @param array $data All form data
173
     */
174
    public function handleAction(GridField $gridField, $actionName, $arguments, $data)
175
    {
176
        switch ($actionName) {
177
            case 'addto':
178
                if (isset($data['relationID']) && $data['relationID']) {
179
                    $gridField->State->GridFieldAddRelation = $data['relationID'];
0 ignored issues
show
Bug Best Practice introduced by
The property GridFieldAddRelation does not exist on SilverStripe\Forms\GridField\GridState_Data. Since you implemented __set, consider adding a @property annotation.
Loading history...
180
                }
181
                break;
182
        }
183
    }
184
185
    /**
186
     * If an object ID is set, add the object to the list
187
     *
188
     * @param GridField $gridField
189
     * @param SS_List $dataList
190
     * @return SS_List
191
     */
192
    public function getManipulatedData(GridField $gridField, SS_List $dataList)
193
    {
194
        $objectID = $gridField->State->GridFieldAddRelation(null);
195
        if (empty($objectID)) {
196
            return $dataList;
197
        }
198
        $object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
199
        if ($object) {
0 ignored issues
show
introduced by
$object is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
200
            $dataList->add($object);
201
        }
202
        $gridField->State->GridFieldAddRelation = null;
0 ignored issues
show
Bug Best Practice introduced by
The property GridFieldAddRelation does not exist on SilverStripe\Forms\GridField\GridState_Data. Since you implemented __set, consider adding a @property annotation.
Loading history...
203
        return $dataList;
204
    }
205
206
    /**
207
     *
208
     * @param GridField $gridField
209
     * @return array
210
     */
211
    public function getURLHandlers($gridField)
212
    {
213
        return [
214
            'search' => 'doSearch',
215
        ];
216
    }
217
218
    /**
219
     * Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
220
     *
221
     * @param GridField $gridField
222
     * @param HTTPRequest $request
223
     * @return string
224
     */
225
    public function doSearch($gridField, $request)
226
    {
227
        $searchStr = $request->getVar('gridfield_relationsearch');
228
        $dataClass = $gridField->getModelClass();
229
230
        $searchFields = ($this->getSearchFields())
231
            ? $this->getSearchFields()
232
            : $this->scaffoldSearchFields($dataClass);
233
        if (!$searchFields) {
234
            throw new LogicException(
235
                sprintf(
236
                    'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"',
237
                    $dataClass
238
                )
239
            );
240
        }
241
242
        $params = [];
243
        foreach ($searchFields as $searchField) {
244
            $name = (strpos($searchField, ':') !== false) ? $searchField : "$searchField:StartsWith";
245
            $params[$name] = $searchStr;
246
        }
247
248
        $results = null;
249
        if ($this->searchList) {
250
            // Assume custom sorting, don't apply default sorting
251
            $results = $this->searchList;
252
        } else {
253
            $results = DataList::create($dataClass)
254
                ->sort(strtok($searchFields[0], ':'), 'ASC');
255
        }
256
257
        // Apply baseline filtering and limits which should hold regardless of any customisations
258
        $results = $results
259
            ->subtract($gridField->getList())
260
            ->filterAny($params)
261
            ->limit($this->getResultsLimit());
262
263
        $json = [];
264
        Config::nest();
265
        SSViewer::config()->update('source_file_comments', false);
266
        $viewer = SSViewer::fromString($this->resultsFormat);
267
        foreach ($results as $result) {
268
            $title = Convert::html2raw($viewer->process($result));
269
            $json[] = [
270
                'label' => $title,
271
                'value' => $title,
272
                'id' => $result->ID,
273
            ];
274
        }
275
        Config::unnest();
276
        $response = new HTTPResponse(json_encode($json));
277
        $response->addHeader('Content-Type', 'application/json');
278
        return $response;
279
    }
280
281
    /**
282
     * @param string $format
283
     *
284
     * @return $this
285
     */
286
    public function setResultsFormat($format)
287
    {
288
        $this->resultsFormat = $format;
289
        return $this;
290
    }
291
292
    /**
293
     * @return string
294
     */
295
    public function getResultsFormat()
296
    {
297
        return $this->resultsFormat;
298
    }
299
300
    /**
301
     * Sets the base list instance which will be used for the autocomplete
302
     * search.
303
     *
304
     * @param SS_List $list
305
     */
306
    public function setSearchList(SS_List $list)
307
    {
308
        $this->searchList = $list;
309
        return $this;
310
    }
311
312
    /**
313
     * @param array $fields
314
     * @return $this
315
     */
316
    public function setSearchFields($fields)
317
    {
318
        $this->searchFields = $fields;
319
        return $this;
320
    }
321
322
    /**
323
     * @return array
324
     */
325
    public function getSearchFields()
326
    {
327
        return $this->searchFields;
328
    }
329
330
    /**
331
     * Detect searchable fields and searchable relations.
332
     * Falls back to {@link DataObject->summaryFields()} if
333
     * no custom search fields are defined.
334
     *
335
     * @param string $dataClass The class name
336
     * @return array|null names of the searchable fields
337
     */
338
    public function scaffoldSearchFields($dataClass)
339
    {
340
        $obj = DataObject::singleton($dataClass);
341
        $fields = null;
342
        if ($fieldSpecs = $obj->searchableFields()) {
343
            $customSearchableFields = $obj->config()->get('searchable_fields');
344
            foreach ($fieldSpecs as $name => $spec) {
345
                if (is_array($spec) && array_key_exists('filter', $spec)) {
346
                    // The searchableFields() spec defaults to PartialMatch,
347
                    // so we need to check the original setting.
348
                    // If the field is defined $searchable_fields = array('MyField'),
349
                    // then default to StartsWith filter, which makes more sense in this context.
350
                    if (!$customSearchableFields || array_search($name, $customSearchableFields)) {
351
                        $filter = 'StartsWith';
352
                    } else {
353
                        $filterName = $spec['filter'];
354
                        // It can be an instance
355
                        if ($filterName instanceof SearchFilter) {
356
                            $filterName = get_class($filterName);
357
                        }
358
                        // It can be a fully qualified class name
359
                        if (strpos($filterName, '\\') !== false) {
360
                            $filterNameParts = explode("\\", $filterName);
361
                            // We expect an alias matching the class name without namespace, see #coresearchaliases
362
                            $filterName = array_pop($filterNameParts);
363
                        }
364
                        $filter = preg_replace('/Filter$/', '', $filterName);
365
                    }
366
                    $fields[] = "{$name}:{$filter}";
367
                } else {
368
                    $fields[] = $name;
369
                }
370
            }
371
        }
372
        if (is_null($fields)) {
0 ignored issues
show
introduced by
The condition is_null($fields) is always true.
Loading history...
373
            if ($obj->hasDatabaseField('Title')) {
374
                $fields = ['Title'];
375
            } elseif ($obj->hasDatabaseField('Name')) {
376
                $fields = ['Name'];
377
            }
378
        }
379
380
        return $fields;
381
    }
382
383
    /**
384
     * @param string $dataClass The class of the object being searched for
385
     *
386
     * @return string
387
     */
388
    public function getPlaceholderText($dataClass)
389
    {
390
        $searchFields = ($this->getSearchFields())
391
            ? $this->getSearchFields()
392
            : $this->scaffoldSearchFields($dataClass);
393
394
        if ($this->placeholderText) {
395
            return $this->placeholderText;
396
        } else {
397
            $labels = [];
398
            if ($searchFields) {
399
                foreach ($searchFields as $searchField) {
400
                    $searchField = explode(':', $searchField);
401
                    $label = singleton($dataClass)->fieldLabel($searchField[0]);
402
                    if ($label) {
403
                        $labels[] = $label;
404
                    }
405
                }
406
            }
407
            if ($labels) {
408
                return _t(
409
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolderWithLabels',
410
                    'Find {type} by {name}',
411
                    ['type' => singleton($dataClass)->i18n_plural_name(), 'name' => implode(', ', $labels)]
412
                );
413
            } else {
414
                return _t(
415
                    'SilverStripe\\Forms\\GridField\\GridField.PlaceHolder',
416
                    'Find {type}',
417
                    ['type' => singleton($dataClass)->i18n_plural_name()]
418
                );
419
            }
420
        }
421
    }
422
423
    /**
424
     * @param string $text
425
     *
426
     * @return $this
427
     */
428
    public function setPlaceholderText($text)
429
    {
430
        $this->placeholderText = $text;
431
        return $this;
432
    }
433
434
    /**
435
     * Gets the maximum number of autocomplete results to display.
436
     *
437
     * @return int
438
     */
439
    public function getResultsLimit()
440
    {
441
        return $this->resultsLimit;
442
    }
443
444
    /**
445
     * @param int $limit
446
     *
447
     * @return $this
448
     */
449
    public function setResultsLimit($limit)
450
    {
451
        $this->resultsLimit = $limit;
452
        return $this;
453
    }
454
}
455