Completed
Push — master ( 55a2d1...8b4cce )
by Daniel
18:24
created

scaffoldSearchFields()   D

Complexity

Conditions 10
Paths 8

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 20
nc 8
nop 1
dl 0
loc 33
rs 4.8196
c 0
b 0
f 0

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
19
/**
20
 * This class is is responsible for adding objects to another object's has_many
21
 * and many_many relation, as defined by the {@link RelationList} passed to the
22
 * {@link GridField} constructor.
23
 *
24
 * Objects can be searched through an input field (partially matching one or
25
 * more fields).
26
 *
27
 * Selecting from the results will add the object to the relation.
28
 *
29
 * Often used alongside {@link GridFieldDeleteAction} for detaching existing
30
 * records from a relationship.
31
 *
32
 * For easier setup, have a look at a sample configuration in
33
 * {@link GridFieldConfig_RelationEditor}.
34
 */
35
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
36
{
37
38
    /**
39
     * The HTML fragment to write this component into
40
     */
41
    protected $targetFragment;
42
43
    /**
44
     * @var SS_List
45
     */
46
    protected $searchList;
47
48
    /**
49
     * Define column names which should be included in the search.
50
     * By default, they're searched with a {@link StartsWithFilter}.
51
     * To define custom filters, use the same notation as {@link DataList->filter()},
52
     * e.g. "Name:EndsWith".
53
     *
54
     * If multiple fields are provided, the filtering is performed non-exclusive.
55
     * If no fields are provided, tries to auto-detect fields from
56
     * {@link DataObject->searchableFields()}.
57
     *
58
     * The fields support "dot-notation" for relationships, e.g.
59
     * a entry called "Team.Name" will search through the names of
60
     * a "Team" relationship.
61
     *
62
     * @example
63
     *  array(
64
     *      'Name',
65
     *      'Email:StartsWith',
66
     *      'Team.Name'
67
     *  )
68
     *
69
     * @var array
70
     */
71
    protected $searchFields = array();
72
73
    /**
74
     * @var string SSViewer template to render the results presentation
75
     */
76
    protected $resultsFormat = '$Title';
77
78
    /**
79
     * @var string Text shown on the search field, instructing what to search for.
80
     */
81
    protected $placeholderText;
82
83
    /**
84
     * @var int
85
     */
86
    protected $resultsLimit = 20;
87
88
    /**
89
     *
90
     * @param string $targetFragment
91
     * @param array $searchFields Which fields on the object in the list should be searched
92
     */
93
    public function __construct($targetFragment = 'before', $searchFields = null)
94
    {
95
        $this->targetFragment = $targetFragment;
96
        $this->searchFields = (array)$searchFields;
97
    }
98
99
    /**
100
     *
101
     * @param GridField $gridField
102
     * @return string[] - HTML
103
     */
104
    public function getHTMLFragments($gridField)
105
    {
106
        $dataClass = $gridField->getModelClass();
107
108
        $forTemplate = new ArrayData(array());
109
        $forTemplate->Fields = new FieldList();
0 ignored issues
show
Documentation introduced by
The property Fields does not exist on object<SilverStripe\View\ArrayData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
110
111
        $searchField = new TextField('gridfield_relationsearch', _t('GridField.RelationSearch', "Relation search"));
112
113
        $searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
114
        $searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
115
        $searchField->addExtraClass('relation-search no-change-track action_gridfield_relationsearch');
116
117
        $findAction = new GridField_FormAction(
118
            $gridField,
119
            'gridfield_relationfind',
120
            _t('GridField.Find', "Find"),
121
            'find',
122
            'find'
123
        );
124
        $findAction->setAttribute('data-icon', 'relationfind');
125
        $findAction->addExtraClass('action_gridfield_relationfind');
126
127
        $addAction = new GridField_FormAction(
128
            $gridField,
129
            'gridfield_relationadd',
130
            _t('GridField.LinkExisting', "Link Existing"),
131
            'addto',
132
            'addto'
133
        );
134
        $addAction->setAttribute('data-icon', 'chain--plus');
135
        $addAction->addExtraClass('btn btn-secondary-outline font-icon-link action_gridfield_relationadd');
136
137
        // If an object is not found, disable the action
138
        if (!is_int($gridField->State->GridFieldAddRelation(null))) {
139
            $addAction->setReadonly(true);
140
        }
141
142
        $forTemplate->Fields->push($searchField);
143
        $forTemplate->Fields->push($findAction);
144
        $forTemplate->Fields->push($addAction);
145
        if ($form = $gridField->getForm()) {
146
            $forTemplate->Fields->setForm($form);
147
        }
148
149
        $template = SSViewer::get_templates_by_class($this, '', __CLASS__);
150
        return array(
151
            $this->targetFragment => $forTemplate->renderWith($template)
152
        );
153
    }
154
155
    /**
156
     *
157
     * @param GridField $gridField
158
     * @return array
159
     */
160
    public function getActions($gridField)
161
    {
162
        return array('addto', 'find');
163
    }
164
165
    /**
166
     * Manipulate the state to add a new relation
167
     *
168
     * @param GridField $gridField
169
     * @param string $actionName Action identifier, see {@link getActions()}.
170
     * @param array $arguments Arguments relevant for this
171
     * @param array $data All form data
172
     */
173
    public function handleAction(GridField $gridField, $actionName, $arguments, $data)
174
    {
175
        switch ($actionName) {
176
            case 'addto':
177
                if (isset($data['relationID']) && $data['relationID']) {
178
                    $gridField->State->GridFieldAddRelation = $data['relationID'];
0 ignored issues
show
Documentation introduced by
The property GridFieldAddRelation does not exist on object<SilverStripe\Form...idField\GridState_Data>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
179
                }
180
                break;
181
        }
182
    }
183
184
    /**
185
     * If an object ID is set, add the object to the list
186
     *
187
     * @param GridField $gridField
188
     * @param SS_List $dataList
189
     * @return SS_List
190
     */
191
    public function getManipulatedData(GridField $gridField, SS_List $dataList)
192
    {
193
        $objectID = $gridField->State->GridFieldAddRelation(null);
194
        if (empty($objectID)) {
195
            return $dataList;
196
        }
197
        $object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
198
        if ($object) {
199
            $dataList->add($object);
200
        }
201
        $gridField->State->GridFieldAddRelation = null;
0 ignored issues
show
Documentation introduced by
The property GridFieldAddRelation does not exist on object<SilverStripe\Form...idField\GridState_Data>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
202
        return $dataList;
203
    }
204
205
    /**
206
     *
207
     * @param GridField $gridField
208
     * @return array
209
     */
210
    public function getURLHandlers($gridField)
211
    {
212
        return array(
213
            'search' => 'doSearch',
214
        );
215
    }
216
217
    /**
218
     * Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
219
     *
220
     * @param GridField $gridField
221
     * @param HTTPRequest $request
222
     * @return string
223
     */
224
    public function doSearch($gridField, $request)
225
    {
226
        $dataClass = $gridField->getModelClass();
227
        $allList = $this->searchList ? $this->searchList : DataList::create($dataClass);
228
229
        $searchFields = ($this->getSearchFields())
230
            ? $this->getSearchFields()
231
            : $this->scaffoldSearchFields($dataClass);
232
        if (!$searchFields) {
233
            throw new LogicException(
234
                sprintf(
235
                    'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"',
236
                    $dataClass
237
                )
238
            );
239
        }
240
241
        $params = array();
242
        foreach ($searchFields as $searchField) {
243
            $name = (strpos($searchField, ':') !== false) ? $searchField : "$searchField:StartsWith";
244
            $params[$name] = $request->getVar('gridfield_relationsearch');
245
        }
246
        $results = $allList
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\ORM\SS_List as the method subtract() does only exist in the following implementations of said interface: SilverStripe\ORM\DataList, SilverStripe\ORM\HasManyList, SilverStripe\ORM\ManyManyList, SilverStripe\ORM\ManyManyThroughList, SilverStripe\ORM\PolymorphicHasManyList, SilverStripe\ORM\RelationList, SilverStripe\Security\Member_GroupSet.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
247
            ->subtract($gridField->getList())
248
            ->filterAny($params)
249
            ->sort(strtok($searchFields[0], ':'), 'ASC')
250
            ->limit($this->getResultsLimit());
251
252
        $json = array();
253
        Config::nest();
254
        SSViewer::config()->update('source_file_comments', false);
255
        $viewer = SSViewer::fromString($this->resultsFormat);
256
        foreach ($results as $result) {
257
            $title = html_entity_decode($viewer->process($result));
258
            $json[] = array(
259
                'label' => $title,
260
                'value' => $title,
261
                'id' => $result->ID,
262
            );
263
        }
264
        Config::unnest();
265
        $response = new HTTPResponse(Convert::array2json($json));
266
        $response->addHeader('Content-Type', 'text/json');
267
        return $response;
268
    }
269
270
    /**
271
     * @param string $format
272
     *
273
     * @return $this
274
     */
275
    public function setResultsFormat($format)
276
    {
277
        $this->resultsFormat = $format;
278
        return $this;
279
    }
280
281
    /**
282
     * @return string
283
     */
284
    public function getResultsFormat()
285
    {
286
        return $this->resultsFormat;
287
    }
288
289
    /**
290
     * Sets the base list instance which will be used for the autocomplete
291
     * search.
292
     *
293
     * @param SS_List $list
294
     */
295
    public function setSearchList(SS_List $list)
296
    {
297
        $this->searchList = $list;
298
        return $this;
299
    }
300
301
    /**
302
     * @param array $fields
303
     * @return $this
304
     */
305
    public function setSearchFields($fields)
306
    {
307
        $this->searchFields = $fields;
308
        return $this;
309
    }
310
311
    /**
312
     * @return array
313
     */
314
    public function getSearchFields()
315
    {
316
        return $this->searchFields;
317
    }
318
319
    /**
320
     * Detect searchable fields and searchable relations.
321
     * Falls back to {@link DataObject->summaryFields()} if
322
     * no custom search fields are defined.
323
     *
324
     * @param string $dataClass The class name
325
     * @return array|null names of the searchable fields
326
     */
327
    public function scaffoldSearchFields($dataClass)
328
    {
329
        $obj = singleton($dataClass);
330
        $fields = null;
331
        if ($fieldSpecs = $obj->searchableFields()) {
332
            $customSearchableFields = $obj->stat('searchable_fields');
333
            foreach ($fieldSpecs as $name => $spec) {
334
                if (is_array($spec) && array_key_exists('filter', $spec)) {
335
                    // The searchableFields() spec defaults to PartialMatch,
336
                    // so we need to check the original setting.
337
                    // If the field is defined $searchable_fields = array('MyField'),
338
                    // then default to StartsWith filter, which makes more sense in this context.
339
                    if (!$customSearchableFields || array_search($name, $customSearchableFields)) {
340
                        $filter = 'StartsWith';
341
                    } else {
342
                        $filter = preg_replace('/Filter$/', '', $spec['filter']);
343
                    }
344
                    $fields[] = "{$name}:{$filter}";
345
                } else {
346
                    $fields[] = $name;
347
                }
348
            }
349
        }
350
        if (is_null($fields)) {
351
            if ($obj->hasDatabaseField('Title')) {
352
                $fields = array('Title');
353
            } elseif ($obj->hasDatabaseField('Name')) {
354
                $fields = array('Name');
355
            }
356
        }
357
358
        return $fields;
359
    }
360
361
    /**
362
     * @param string $dataClass The class of the object being searched for
363
     *
364
     * @return string
365
     */
366
    public function getPlaceholderText($dataClass)
367
    {
368
        $searchFields = ($this->getSearchFields())
369
            ? $this->getSearchFields()
370
            : $this->scaffoldSearchFields($dataClass);
371
372
        if ($this->placeholderText) {
373
            return $this->placeholderText;
374
        } else {
375
            $labels = array();
376
            if ($searchFields) {
377
                foreach ($searchFields as $searchField) {
378
                    $searchField = explode(':', $searchField);
379
                    $label = singleton($dataClass)->fieldLabel($searchField[0]);
380
                    if ($label) {
381
                        $labels[] = $label;
382
                    }
383
                }
384
            }
385
            if ($labels) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $labels 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...
386
                return _t(
387
                    'GridField.PlaceHolderWithLabels',
388
                    'Find {type} by {name}',
389
                    array('type' => singleton($dataClass)->i18n_plural_name(), 'name' => implode(', ', $labels))
390
                );
391
            } else {
392
                return _t(
393
                    'GridField.PlaceHolder',
394
                    'Find {type}',
395
                    array('type' => singleton($dataClass)->i18n_plural_name())
396
                );
397
            }
398
        }
399
    }
400
401
    /**
402
     * @param string $text
403
     *
404
     * @return $this
405
     */
406
    public function setPlaceholderText($text)
407
    {
408
        $this->placeholderText = $text;
409
        return $this;
410
    }
411
412
    /**
413
     * Gets the maximum number of autocomplete results to display.
414
     *
415
     * @return int
416
     */
417
    public function getResultsLimit()
418
    {
419
        return $this->resultsLimit;
420
    }
421
422
    /**
423
     * @param int $limit
424
     *
425
     * @return $this
426
     */
427
    public function setResultsLimit($limit)
428
    {
429
        $this->resultsLimit = $limit;
430
        return $this;
431
    }
432
}
433