Passed
Pull Request — 4.8 (#10055)
by Steve
07:13
created

SearchContext::getQuery()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 54
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 33
c 1
b 0
f 0
nc 10
nop 4
dl 0
loc 54
rs 6.9666

How to fix   Long Method    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\ORM\Search;
4
5
use SilverStripe\Control\HTTPRequest;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\FormField;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\DataList;
12
use SilverStripe\ORM\Filters\SearchFilter;
13
use SilverStripe\ORM\ArrayList;
14
use SilverStripe\View\ArrayData;
15
use SilverStripe\Forms\SelectField;
16
use SilverStripe\Forms\CheckboxField;
17
use InvalidArgumentException;
18
use Exception;
19
20
/**
21
 * Manages searching of properties on one or more {@link DataObject}
22
 * types, based on a given set of input parameters.
23
 * SearchContext is intentionally decoupled from any controller-logic,
24
 * it just receives a set of search parameters and an object class it acts on.
25
 *
26
 * The default output of a SearchContext is either a {@link SQLSelect} object
27
 * for further refinement, or a {@link SS_List} that can be used to display
28
 * search results, e.g. in a {@link TableListField} instance.
29
 *
30
 * In case you need multiple contexts, consider namespacing your request parameters
31
 * by using {@link FieldList->namespace()} on the $fields constructor parameter.
32
 *
33
 * Each DataObject subclass can have multiple search contexts for different cases,
34
 * e.g. for a limited frontend search and a fully featured backend search.
35
 * By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically
36
 * scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields
37
 * to include.
38
 *
39
 * @see http://doc.silverstripe.com/doku.php?id=searchcontext
40
 */
41
class SearchContext
42
{
43
    use Injectable;
44
45
    /**
46
     * DataObject subclass to which search parameters relate to.
47
     * Also determines as which object each result is provided.
48
     *
49
     * @var string
50
     */
51
    protected $modelClass;
52
53
    /**
54
     * FormFields mapping to {@link DataObject::$db} properties
55
     * which are supposed to be searchable.
56
     *
57
     * @var FieldList
58
     */
59
    protected $fields;
60
61
    /**
62
     * Array of {@link SearchFilter} subclasses.
63
     *
64
     * @var SearchFilter[]
65
     */
66
    protected $filters;
67
68
    /**
69
     * Key/value pairs of search fields to search terms
70
     *
71
     * @var array
72
     */
73
    protected $searchParams = [];
74
75
    /**
76
     * The logical connective used to join WHERE clauses. Defaults to AND.
77
     * @var string
78
     */
79
    public $connective = 'AND';
80
81
    /**
82
     * A key value pair of values that should be searched for.
83
     * The keys should match the field names specified in {@link self::$fields}.
84
     * Usually these values come from a submitted searchform
85
     * in the form of a $_REQUEST object.
86
     * CAUTION: All values should be treated as insecure client input.
87
     *
88
     * @param string $modelClass The base {@link DataObject} class that search properties related to.
89
     *                      Also used to generate a set of result objects based on this class.
90
     * @param FieldList $fields Optional. FormFields mapping to {@link DataObject::$db} properties
91
     *                      which are to be searched. Derived from modelclass using
92
     *                      {@link DataObject::scaffoldSearchFields()} if left blank.
93
     * @param array $filters Optional. Derived from modelclass if left blank
94
     */
95
    public function __construct($modelClass, $fields = null, $filters = null)
96
    {
97
        $this->modelClass = $modelClass;
98
        $this->fields = ($fields) ? $fields : new FieldList();
99
        $this->filters = ($filters) ? $filters : [];
100
    }
101
102
    /**
103
     * Returns scaffolded search fields for UI.
104
     *
105
     * @return FieldList
106
     */
107
    public function getSearchFields()
108
    {
109
        return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
110
        // $this->fields is causing weirdness, so we ignore for now, using the default scaffolding
111
        //return singleton($this->modelClass)->scaffoldSearchFields();
112
    }
113
114
    /**
115
     * @todo move to SQLSelect
116
     * @todo fix hack
117
     */
118
    protected function applyBaseTableFields()
119
    {
120
        $classes = ClassInfo::dataClassesFor($this->modelClass);
121
        $baseTable = DataObject::getSchema()->baseDataTable($this->modelClass);
122
        $fields = ["\"{$baseTable}\".*"];
123
        if ($this->modelClass != $classes[0]) {
124
            $fields[] = '"' . $classes[0] . '".*';
125
        }
126
        //$fields = array_keys($model->db());
127
        $fields[] = '"' . $classes[0] . '".\"ClassName\" AS "RecordClassName"';
128
        return $fields;
129
    }
130
131
    /**
132
     * Returns a SQL object representing the search context for the given
133
     * list of query parameters.
134
     *
135
     * @param array $searchParams Map of search criteria, mostly taken from $_REQUEST.
136
     *  If a filter is applied to a relationship in dot notation,
137
     *  the parameter name should have the dots replaced with double underscores,
138
     *  for example "Comments__Name" instead of the filter name "Comments.Name".
139
     * @param array|bool|string $sort Database column to sort on.
140
     *  Falls back to {@link DataObject::$default_sort} if not provided.
141
     * @param array|bool|string $limit
142
     * @param DataList $existingQuery
143
     * @return DataList
144
     * @throws Exception
145
     */
146
    public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null)
147
    {
148
        /** DataList $query */
149
        $query = null;
150
        if ($existingQuery) {
151
            if (!($existingQuery instanceof DataList)) {
0 ignored issues
show
introduced by
$existingQuery is always a sub-type of SilverStripe\ORM\DataList.
Loading history...
152
                throw new InvalidArgumentException("existingQuery must be DataList");
153
            }
154
            if ($existingQuery->dataClass() != $this->modelClass) {
155
                throw new InvalidArgumentException("existingQuery's dataClass is " . $existingQuery->dataClass()
156
                    . ", $this->modelClass expected.");
157
            }
158
            $query = $existingQuery;
159
        } else {
160
            $query = DataList::create($this->modelClass);
161
        }
162
163
        if (is_array($limit)) {
164
            $query = $query->limit(
165
                isset($limit['limit']) ? $limit['limit'] : null,
166
                isset($limit['start']) ? $limit['start'] : null
167
            );
168
        } else {
169
            $query = $query->limit($limit);
0 ignored issues
show
Bug introduced by
$limit of type boolean|string is incompatible with the type integer expected by parameter $limit of SilverStripe\ORM\DataList::limit(). ( Ignorable by Annotation )

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

169
            $query = $query->limit(/** @scrutinizer ignore-type */ $limit);
Loading history...
170
        }
171
172
        /** @var DataList $query */
173
        $query = $query->sort($sort);
174
        $this->setSearchParams($searchParams); // << i think this resets the search values on the front end form?
175
176
        // sboyd
177
        foreach ($searchParams as $field => $values) {
178
            $searchParams[$field . ':PartialMatch'] = $values;
179
            unset($searchParams[$field]);
180
        }
181
        $query = $query->filterAny($searchParams);
182
        return $query;
183
184
        foreach ($this->searchParams as $key => $value) {
0 ignored issues
show
Unused Code introduced by
ForeachNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
185
            $key = str_replace('__', '.', $key);
186
            if ($filter = $this->getFilter($key)) {
187
                $filter->setModel($this->modelClass);
188
                $filter->setValue($value);
189
                if (!$filter->isEmpty()) {
190
                    $query = $query->alterDataQuery([$filter, 'apply']);
191
                }
192
            }
193
        }
194
195
        if ($this->connective != "AND") {
196
            throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
197
        }
198
199
        return $query;
200
    }
201
202
    /**
203
     * Returns a result set from the given search parameters.
204
     *
205
     * @todo rearrange start and limit params to reflect DataObject
206
     *
207
     * @param array $searchParams
208
     * @param array|bool|string $sort
209
     * @param array|bool|string $limit
210
     * @return DataList
211
     * @throws Exception
212
     */
213
    public function getResults($searchParams, $sort = false, $limit = false)
214
    {
215
        $searchParams = array_filter((array)$searchParams, [$this, 'clearEmptySearchFields']);
216
217
        // getQuery actually returns a DataList
218
        return $this->getQuery($searchParams, $sort, $limit);
219
    }
220
221
    /**
222
     * Callback map function to filter fields with empty values from
223
     * being included in the search expression.
224
     *
225
     * @param mixed $value
226
     * @return boolean
227
     */
228
    public function clearEmptySearchFields($value)
229
    {
230
        return ($value != '');
231
    }
232
233
    /**
234
     * Accessor for the filter attached to a named field.
235
     *
236
     * @param string $name
237
     * @return SearchFilter
238
     */
239
    public function getFilter($name)
240
    {
241
        if (isset($this->filters[$name])) {
242
            return $this->filters[$name];
243
        } else {
244
            return null;
245
        }
246
    }
247
248
    /**
249
     * Get the map of filters in the current search context.
250
     *
251
     * @return SearchFilter[]
252
     */
253
    public function getFilters()
254
    {
255
        return $this->filters;
256
    }
257
258
    /**
259
     * Overwrite the current search context filter map.
260
     *
261
     * @param array $filters
262
     */
263
    public function setFilters($filters)
264
    {
265
        $this->filters = $filters;
266
    }
267
268
    /**
269
     * Adds a instance of {@link SearchFilter}.
270
     *
271
     * @param SearchFilter $filter
272
     */
273
    public function addFilter($filter)
274
    {
275
        $this->filters[$filter->getFullName()] = $filter;
276
    }
277
278
    /**
279
     * Removes a filter by name.
280
     *
281
     * @param string $name
282
     */
283
    public function removeFilterByName($name)
284
    {
285
        unset($this->filters[$name]);
286
    }
287
288
    /**
289
     * Get the list of searchable fields in the current search context.
290
     *
291
     * @return FieldList
292
     */
293
    public function getFields()
294
    {
295
        return $this->fields;
296
    }
297
298
    /**
299
     * Apply a list of searchable fields to the current search context.
300
     *
301
     * @param FieldList $fields
302
     */
303
    public function setFields($fields)
304
    {
305
        $this->fields = $fields;
306
    }
307
308
    /**
309
     * Adds a new {@link FormField} instance.
310
     *
311
     * @param FormField $field
312
     */
313
    public function addField($field)
314
    {
315
        $this->fields->push($field);
316
    }
317
318
    /**
319
     * Removes an existing formfield instance by its name.
320
     *
321
     * @param string $fieldName
322
     */
323
    public function removeFieldByName($fieldName)
324
    {
325
        $this->fields->removeByName($fieldName);
326
    }
327
328
    /**
329
     * Set search param values
330
     *
331
     * @param array|HTTPRequest $searchParams
332
     * @return $this
333
     */
334
    public function setSearchParams($searchParams)
335
    {
336
        // hack to work with $searchParams when it's an Object
337
        if ($searchParams instanceof HTTPRequest) {
338
            $this->searchParams = $searchParams->getVars();
339
        } else {
340
            $this->searchParams = $searchParams;
341
        }
342
        return $this;
343
    }
344
345
    /**
346
     * @return array
347
     */
348
    public function getSearchParams()
349
    {
350
        return $this->searchParams;
351
    }
352
353
    /**
354
     * Gets a list of what fields were searched and the values provided
355
     * for each field. Returns an ArrayList of ArrayData, suitable for
356
     * rendering on a template.
357
     *
358
     * @return ArrayList
359
     */
360
    public function getSummary()
361
    {
362
        $list = ArrayList::create();
363
        foreach ($this->searchParams as $searchField => $searchValue) {
364
            if (empty($searchValue)) {
365
                continue;
366
            }
367
            $filter = $this->getFilter($searchField);
368
            if (!$filter) {
369
                continue;
370
            }
371
372
            $field = $this->fields->fieldByName($filter->getFullName());
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $this->fields->fieldByNa...$filter->getFullName()) targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
373
            if (!$field) {
374
                continue;
375
            }
376
377
            // For dropdowns, checkboxes, etc, get the value that was presented to the user
378
            // e.g. not an ID
379
            if ($field instanceof SelectField) {
380
                $source = $field->getSource();
381
                if (isset($source[$searchValue])) {
382
                    $searchValue = $source[$searchValue];
383
                }
384
            } else {
385
                // For checkboxes, it suffices to simply include the field in the list, since it's binary
386
                if ($field instanceof CheckboxField) {
387
                    $searchValue = null;
388
                }
389
            }
390
391
            $list->push(ArrayData::create([
392
                'Field' => $field->Title(),
393
                'Value' => $searchValue,
394
            ]));
395
        }
396
397
        return $list;
398
    }
399
}
400