Completed
Push — master ( 8dc27d...415fde )
by Robbie
03:50 queued 02:06
created

SearchIndex::addClass()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 2
nop 2
1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Indexes;
4
5
use Exception;
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\FullTextSearch\Search\SearchIntrospection;
11
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
12
use SilverStripe\FullTextSearch\Utils\MultipleArrayIterator;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\FieldType\DBField;
15
use SilverStripe\ORM\FieldType\DBString;
16
use SilverStripe\ORM\Queries\SQLSelect;
17
use SilverStripe\View\ViewableData;
18
19
/**
20
 * SearchIndex is the base index class. Each connector will provide a subclass of this that
21
 * provides search engine specific behavior.
22
 *
23
 * This class is responsible for:
24
 *
25
 * - Taking index calls adding classes and fields, and resolving those to value sources and types
26
 *
27
 * - Determining which records in this index need updating when a DataObject is changed
28
 *
29
 * - Providing utilities to the connector indexes
30
 *
31
 * The connector indexes are responsible for
32
 *
33
 * - Mapping types to index configuration
34
 *
35
 * - Adding and removing items to index
36
 *
37
 * - Parsing and converting SearchQueries into a form the engine will understand, and executing those queries
38
 *
39
 * The user indexes are responsible for
40
 *
41
 * - Specifying which classes and fields this index contains
42
 *
43
 * - Specifying update rules that are not extractable from metadata (because the values come from functions for instance)
44
 *
45
 */
46
abstract class SearchIndex extends ViewableData
47
{
48
    /**
49
     * Allows this index to hide a parent index. Specifies the name of a parent index to disable
50
     *
51
     * @var string
52
     * @config
53
     */
54
    private static $hide_ancestor;
0 ignored issues
show
Unused Code introduced by
The property $hide_ancestor is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
55
56
    /**
57
     * Used to separate class name and relation name in the sources array
58
     * this string must not be present in class name
59
     * @var string
60
     * @config
61
     */
62
    private static $class_delimiter = '_|_';
0 ignored issues
show
Unused Code introduced by
The property $class_delimiter is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
63
64
    /**
65
     * This is used to clean the source name from suffix
66
     * suffixes are needed to support multiple relations with the same name on different page types
67
     * @param string $source
68
     * @return string
69
     */
70
    protected function getSourceName($source)
71
    {
72
        $source = explode(self::config()->get('class_delimiter'), $source);
73
74
        return $source[0];
75
    }
76
77
    public function __construct()
78
    {
79
        parent::__construct();
80
        $this->init();
81
82
        foreach ($this->getClasses() as $class => $options) {
83
            SearchVariant::with($class, $options['include_children'])->call('alterDefinition', $class, $this);
84
        }
85
86
        $this->buildDependancyList();
87
    }
88
89
    public function __toString()
90
    {
91
        return 'Search Index ' . get_class($this);
92
    }
93
94
    /**
95
     * Examines the classes this index is built on to try and find defined fields in the class hierarchy
96
     * for those classes.
97
     * Looks for db and viewable-data fields, although can't necessarily find type for viewable-data fields.
98
     * If multiple classes have a relation with the same name all of these will be included in the search index
99
     * Note that only classes that have the relations uninherited (defined in them) will be listed
100
     * this is because inherited relations do not need to be processed by index explicitly
101
     */
102
    public function fieldData($field, $forceType = null, $extraOptions = [])
103
    {
104
        $fullfield = str_replace(".", "_", $field);
105
        $sources = $this->getClasses();
106
107
        foreach ($sources as $source => $options) {
108
            $sources[$source]['base'] = DataObject::getSchema()->baseDataClass($source);
109
            $sources[$source]['lookup_chain'] = [];
110
        }
111
112
        $found = [];
113
114
        if (strpos($field, '.') !== false) {
115
            $lookups = explode(".", $field);
116
            $field = array_pop($lookups);
117
118
            foreach ($lookups as $lookup) {
119
                $next = [];
120
121
                foreach ($sources as $source => $baseOptions) {
122
                    $source = $this->getSourceName($source);
123
124
                    foreach (SearchIntrospection::hierarchy($source, $baseOptions['include_children']) as $dataclass) {
125
                        $class = null;
126
                        $options = $baseOptions;
127
                        $singleton = singleton($dataclass);
128
                        $schema = DataObject::getSchema();
129
                        $className = $singleton->getClassName();
130
131
                        if ($hasOne = $schema->hasOneComponent($className, $lookup)) {
132
                            // we only want to include base class for relation, omit classes that inherited the relation
133
                            $relationList = Config::inst()->get($dataclass, 'has_one', Config::UNINHERITED);
134
                            $relationList = (!is_null($relationList)) ? $relationList : [];
135
                            if (!array_key_exists($lookup, $relationList)) {
136
                                continue;
137
                            }
138
139
                            $class = $hasOne;
140
                            $options['lookup_chain'][] = array(
141
                                'call' => 'method', 'method' => $lookup,
142
                                'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID"
143
                            );
144
                        } elseif ($hasMany = $schema->hasManyComponent($className, $lookup)) {
145
                            // we only want to include base class for relation, omit classes that inherited the relation
146
                            $relationList = Config::inst()->get($dataclass, 'has_many', Config::UNINHERITED);
147
                            $relationList = (!is_null($relationList)) ? $relationList : [];
148
                            if (!array_key_exists($lookup, $relationList)) {
149
                                continue;
150
                            }
151
152
                            $class = $hasMany;
153
                            $options['multi_valued'] = true;
154
                            $options['lookup_chain'][] = array(
155
                                'call' => 'method', 'method' => $lookup,
156
                                'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $schema->getRemoteJoinField($className, $lookup, 'has_many')
157
                            );
158
                        } elseif ($manyMany = $schema->manyManyComponent($className, $lookup)) {
159
                            // we only want to include base class for relation, omit classes that inherited the relation
160
                            $relationList = Config::inst()->get($dataclass, 'many_many', Config::UNINHERITED);
161
                            $relationList = (!is_null($relationList)) ? $relationList : [];
162
                            if (!array_key_exists($lookup, $relationList)) {
163
                                continue;
164
                            }
165
166
                            $class = $manyMany['childClass'];
167
                            $options['multi_valued'] = true;
168
                            $options['lookup_chain'][] = array(
169
                                'call' => 'method',
170
                                'method' => $lookup,
171
                                'through' => 'many_many',
172
                                'class' => $dataclass,
173
                                'otherclass' => $class,
174
                                'details' => $manyMany,
175
                            );
176
                        }
177
178
                        if (is_string($class) && $class) {
179
                            if (!isset($options['origin'])) {
180
                                $options['origin'] = $dataclass;
181
                            }
182
183
                            // we add suffix here to prevent the relation to be overwritten by other instances
184
                            // all sources lookups must clean the source name before reading it via getSourceName()
185
                            $next[$class . self::config()->get('class_delimiter') . $dataclass] = $options;
186
                        }
187
                    }
188
                }
189
190
                if (!$next) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $next 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...
191
                    return $next;
192
                } // Early out to avoid excessive empty looping
193
                $sources = $next;
194
            }
195
        }
196
197
        foreach ($sources as $class => $options) {
198
            $class = $this->getSourceName($class);
199
            $dataclasses = SearchIntrospection::hierarchy($class, $options['include_children']);
200
201
            while (count($dataclasses)) {
202
                $dataclass = array_shift($dataclasses);
203
                $type = null;
204
                $fieldoptions = $options;
205
206
                $fields = DataObject::getSchema()->databaseFields($class);
207
208
                if (isset($fields[$field])) {
209
                    $type = $fields[$field];
210
                    $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
211
                } else {
212
                    $singleton = singleton($dataclass);
213
214
                    if ($singleton->hasMethod("get$field") || $singleton->hasField($field)) {
215
                        $type = $singleton->castingClass($field);
216
                        if (!$type) {
217
                            $type = 'String';
218
                        }
219
220
                        if ($singleton->hasMethod("get$field")) {
221
                            $fieldoptions['lookup_chain'][] = array('call' => 'method', 'method' => "get$field");
222
                        } else {
223
                            $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
224
                        }
225
                    }
226
                }
227
228
                if ($type) {
229
                    // Don't search through child classes of a class we matched on. TODO: Should we?
230
                    $dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass)));
231
                    // Trim arguments off the type string
232
                    if (preg_match('/^(\w+)\(/', $type, $match)) {
233
                        $type = $match[1];
234
                    }
235
                    // Get the origin
236
                    $origin = isset($fieldoptions['origin']) ? $fieldoptions['origin'] : $dataclass;
237
238
                    $found["{$origin}_{$fullfield}"] = array(
239
                        'name' => "{$origin}_{$fullfield}",
240
                        'field' => $field,
241
                        'fullfield' => $fullfield,
242
                        'base' => $fieldoptions['base'],
243
                        'origin' => $origin,
244
                        'class' => $dataclass,
245
                        'lookup_chain' => $fieldoptions['lookup_chain'],
246
                        'type' => $forceType ? $forceType : $type,
247
                        'multi_valued' => isset($fieldoptions['multi_valued']) ? true : false,
248
                        'extra_options' => $extraOptions
249
                    );
250
                }
251
            }
252
        }
253
254
        return $found;
255
    }
256
257
    /** Public, but should only be altered by variants */
258
259
    protected $classes = array();
260
261
    protected $fulltextFields = array();
262
263
    public $filterFields = array();
264
265
    protected $sortFields = array();
266
267
    protected $excludedVariantStates = array();
268
269
    /**
270
     * Add a DataObject subclass whose instances should be included in this index
271
     *
272
     * Can only be called when addFulltextField, addFilterField, addSortField and addAllFulltextFields have not
273
     * yet been called for this index instance
274
     *
275
     * @throws Exception
276
     * @param string $class - The class to include
277
     * @param array $options - TODO: Remove
278
     */
279
    public function addClass($class, $options = array())
280
    {
281
        if ($this->fulltextFields || $this->filterFields || $this->sortFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fulltextFields 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...
Bug Best Practice introduced by
The expression $this->filterFields 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...
Bug Best Practice introduced by
The expression $this->sortFields 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...
282
            throw new Exception('Can\'t add class to Index after fields have already been added');
283
        }
284
285
        $options = array_merge(array(
286
            'include_children' => true
287
        ), $options);
288
289
        $this->classes[$class] = $options;
290
    }
291
292
    /**
293
     * Get the classes added by addClass
294
     */
295
    public function getClasses()
296
    {
297
        return $this->classes;
298
    }
299
300
    /**
301
     * Add a field that should be fulltext searchable
302
     * @param string $field - The field to add
303
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
304
     * @param string $extraOptions - Dependent on search implementation
305
     */
306
    public function addFulltextField($field, $forceType = null, $extraOptions = array())
307
    {
308
        $this->fulltextFields = array_merge($this->fulltextFields, $this->fieldData($field, $forceType, $extraOptions));
0 ignored issues
show
Bug introduced by
It seems like $extraOptions defined by parameter $extraOptions on line 306 can also be of type string; however, SilverStripe\FullTextSea...earchIndex::fieldData() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
309
    }
310
311
    public function getFulltextFields()
312
    {
313
        return $this->fulltextFields;
314
    }
315
316
    /**
317
     * Add a field that should be filterable
318
     * @param string $field - The field to add
319
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
320
     * @param string $extraOptions - Dependent on search implementation
321
     */
322
    public function addFilterField($field, $forceType = null, $extraOptions = array())
323
    {
324
        $this->filterFields = array_merge($this->filterFields, $this->fieldData($field, $forceType, $extraOptions));
0 ignored issues
show
Bug introduced by
It seems like $extraOptions defined by parameter $extraOptions on line 322 can also be of type string; however, SilverStripe\FullTextSea...earchIndex::fieldData() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
325
    }
326
327
    public function getFilterFields()
328
    {
329
        return $this->filterFields;
330
    }
331
332
    /**
333
     * Add a field that should be sortable
334
     * @param string $field - The field to add
335
     * @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
336
     * @param string $extraOptions - Dependent on search implementation
337
     */
338
    public function addSortField($field, $forceType = null, $extraOptions = array())
339
    {
340
        $this->sortFields = array_merge($this->sortFields, $this->fieldData($field, $forceType, $extraOptions));
0 ignored issues
show
Bug introduced by
It seems like $extraOptions defined by parameter $extraOptions on line 338 can also be of type string; however, SilverStripe\FullTextSea...earchIndex::fieldData() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
341
    }
342
343
    public function getSortFields()
344
    {
345
        return $this->sortFields;
346
    }
347
348
    /**
349
     * Add all database-backed text fields as fulltext searchable fields.
350
     *
351
     * For every class included in the index, examines those classes and all subclasses looking for "Text" database
352
     * fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields.
353
     */
354
    public function addAllFulltextFields($includeSubclasses = true)
355
    {
356
        foreach ($this->getClasses() as $class => $options) {
357
            $classHierarchy = SearchIntrospection::hierarchy($class, $includeSubclasses, true);
358
359
            foreach ($classHierarchy as $dataClass) {
360
                $fields = DataObject::getSchema()->databaseFields($dataClass);
361
362
                foreach ($fields as $field => $type) {
363
                    list($type, $args) = ClassInfo::parse_class_spec($type);
0 ignored issues
show
Unused Code introduced by
The assignment to $args is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
364
365
                    /** @var DBField $object */
366
                    $object = Injector::inst()->get($type, false, ['Name' => 'test']);
367
                    if ($object instanceof DBString) {
368
                        $this->addFulltextField($field);
369
                    }
370
                }
371
            }
372
        }
373
    }
374
375
    /**
376
     * Returns an interator that will let you interate through all added fields, regardless of whether they
377
     * were added as fulltext, filter or sort fields.
378
     *
379
     * @return MultipleArrayIterator
380
     */
381
    public function getFieldsIterator()
382
    {
383
        return new MultipleArrayIterator($this->fulltextFields, $this->filterFields, $this->sortFields);
384
    }
385
386
    public function excludeVariantState($state)
387
    {
388
        $this->excludedVariantStates[] = $state;
389
    }
390
391
    /** Returns true if some variant state should be ignored */
392
    public function variantStateExcluded($state)
393
    {
394
        foreach ($this->excludedVariantStates as $excludedstate) {
395
            $matches = true;
396
397
            foreach ($excludedstate as $variant => $variantstate) {
398
                if (!isset($state[$variant]) || $state[$variant] != $variantstate) {
399
                    $matches = false;
400
                    break;
401
                }
402
            }
403
404
            if ($matches) {
405
                return true;
406
            }
407
        }
408
    }
409
410
    public $dependancyList = array();
411
412
    public function buildDependancyList()
413
    {
414
        $this->dependancyList = array_keys($this->getClasses());
415
416
        foreach ($this->getFieldsIterator() as $name => $field) {
417
            if (!isset($field['class'])) {
418
                continue;
419
            }
420
            SearchIntrospection::add_unique_by_ancestor($this->dependancyList, $field['class']);
421
        }
422
    }
423
424
    public $derivedFields = null;
425
426
    /**
427
     * Returns an array where each member is all the fields and the classes that are at the end of some
428
     * specific lookup chain from one of the base classes
429
     */
430
    public function getDerivedFields()
431
    {
432
        if ($this->derivedFields === null) {
433
            $this->derivedFields = array();
434
435
            foreach ($this->getFieldsIterator() as $name => $field) {
436
                if (count($field['lookup_chain']) < 2) {
437
                    continue;
438
                }
439
440
                $key = sha1($field['base'].serialize($field['lookup_chain']));
441
                $fieldname = "{$field['class']}:{$field['field']}";
442
443
                if (isset($this->derivedFields[$key])) {
444
                    $this->derivedFields[$key]['fields'][$fieldname] = $fieldname;
445
                    SearchIntrospection::add_unique_by_ancestor($this->derivedFields['classes'], $field['class']);
446
                } else {
447
                    $chain = array_reverse($field['lookup_chain']);
448
                    array_shift($chain);
449
450
                    $this->derivedFields[$key] = array(
451
                        'base' => $field['base'],
452
                        'fields' => array($fieldname => $fieldname),
453
                        'classes' => array($field['class']),
454
                        'chain' => $chain
455
                    );
456
                }
457
            }
458
        }
459
460
        return $this->derivedFields;
461
    }
462
463
    /**
464
     * Get the "document ID" (a database & variant unique id) given some "Base" class, DataObject ID and state array
465
     *
466
     * @param string $base - The base class of the object
467
     * @param integer $id - The ID of the object
468
     * @param array $state - The variant state of the object
469
     * @return string - The document ID as a string
470
     */
471
    public function getDocumentIDForState($base, $id, $state)
472
    {
473
        ksort($state);
474
        $parts = array('id' => $id, 'base' => $base, 'state' => json_encode($state));
475
        return implode('-', array_values($parts));
476
    }
477
478
    /**
479
     * Get the "document ID" (a database & variant unique id) given some "Base" class and DataObject
480
     *
481
     * @param DataObject $object - The object
482
     * @param string $base - The base class of the object
483
     * @param boolean $includesubs - TODO: Probably going away
484
     * @return string - The document ID as a string
485
     */
486
    public function getDocumentID($object, $base, $includesubs)
487
    {
488
        return $this->getDocumentIDForState($base, $object->ID, SearchVariant::current_state($base, $includesubs));
489
    }
490
491
    /**
492
     * Given an object and a field definition (as returned by fieldData) get the current value of that field on that object
493
     *
494
     * @param DataObject $object - The object to get the value from
495
     * @param array $field - The field definition to use
496
     * @return mixed - The value of the field, or null if we couldn't look it up for some reason
497
     */
498
    protected function _getFieldValue($object, $field)
499
    {
500
        $errorHandler = function ($no, $str) {
501
            throw new Exception('HTML Parse Error: ' . $str);
502
        };
503
        set_error_handler($errorHandler, E_ALL);
504
505
        try {
506
            foreach ($field['lookup_chain'] as $step) {
507
                // Just fail if we've fallen off the end of the chain
508
                if (!$object) {
509
                    return null;
510
                }
511
512
                // If we're looking up this step on an array or SS_List, do the step on every item, merge result
513
                if (is_array($object) || $object instanceof SS_List) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\FullTextSearch\Search\Indexes\SS_List does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
514
                    $next = array();
515
516
                    foreach ($object as $item) {
517
                        if ($step['call'] == 'method') {
518
                            $method = $step['method'];
519
                            $item = $item->$method();
520
                        } else {
521
                            $property = $step['property'];
522
                            $item = $item->$property;
523
                        }
524
525
                        if ($item instanceof SS_List) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\FullTextSearch\Search\Indexes\SS_List does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
526
                            $next = array_merge($next, $item->toArray());
527
                        } elseif (is_array($item)) {
528
                            $next = array_merge($next, $item);
529
                        } else {
530
                            $next[] = $item;
531
                        }
532
                    }
533
534
                    $object = $next;
535
                } else {
536
                    // Otherwise, just call
537
                    if ($step['call'] == 'method') {
538
                        $method = $step['method'];
539
                        $object = $object->$method();
540
                    } elseif ($step['call'] == 'variant') {
541
                        $variants = SearchVariant::variants();
542
                        $variant = $variants[$step['variant']];
543
                        $method = $step['method'];
544
                        $object = $variant->$method($object);
545
                    } else {
546
                        $property = $step['property'];
547
                        $object = $object->$property;
548
                    }
549
                }
550
            }
551
        } catch (Exception $e) {
552
            static::warn($e);
553
            $object = null;
554
        }
555
556
        restore_error_handler();
557
        return $object;
558
    }
559
560
    /**
561
     * Log non-fatal errors
562
     *
563
     * @param Exception $e
564
     */
565
    public static function warn($e)
566
    {
567
        Injector::inst()->get(LoggerInterface::class)->warning($e);
568
    }
569
570
    /**
571
     * Given a class, object id, set of stateful ids and a list of changed fields (in a special format),
572
     * return what statefulids need updating in this index
573
     *
574
     * Internal function used by SearchUpdater.
575
     *
576
     * @param  string $class
577
     * @param  int $id
578
     * @param  array $statefulids
579
     * @param  array $fields
580
     * @return array
581
     */
582
    public function getDirtyIDs($class, $id, $statefulids, $fields)
583
    {
584
        $dirty = array();
585
586
        // First, if this object is directly contained in the index, add it
587
        foreach ($this->classes as $searchclass => $options) {
588
            if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $searchclass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
589
                $base = DataObject::getSchema()->baseDataClass($searchclass);
590
                $dirty[$base] = array();
591
                foreach ($statefulids as $statefulid) {
592
                    $key = serialize($statefulid);
593
                    $dirty[$base][$key] = $statefulid;
594
                }
595
            }
596
        }
597
598
        $current = SearchVariant::current_state();
599
600
601
        // Then, for every derived field
602
        foreach ($this->getDerivedFields() as $derivation) {
603
            // If the this object is a subclass of any of the classes we want a field from
604
            if (!SearchIntrospection::is_subclass_of($class, $derivation['classes'])) {
605
                continue;
606
            }
607
            if (!array_intersect_key($fields, $derivation['fields'])) {
608
                continue;
609
            }
610
611
            foreach (SearchVariant::reindex_states($class, false) as $state) {
612
                SearchVariant::activate_state($state);
613
614
                $ids = array($id);
615
616
                foreach ($derivation['chain'] as $step) {
617
                    // Use TableName for queries
618
                    $tableName = DataObject::getSchema()->tableName($step['class']);
619
620
                    if ($step['through'] == 'has_one') {
621
                        $sql = new SQLSelect('"ID"', '"'.$tableName.'"', '"'.$step['foreignkey'].'" IN ('.implode(',', $ids).')');
0 ignored issues
show
Documentation introduced by
'"' . $step['foreignkey'...mplode(',', $ids) . ')' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
622
                        singleton($step['class'])->extend('augmentSQL', $sql);
623
624
                        $ids = $sql->execute()->column();
625
                    } elseif ($step['through'] == 'has_many') {
626
                        // Use TableName for queries
627
                        $otherTableName = DataObject::getSchema()->tableName($step['otherclass']);
628
629
                        $sql = new SQLSelect('"'.$tableName.'"."ID"', '"'.$tableName.'"', '"'.$otherTableName.'"."ID" IN ('.implode(',', $ids).')');
0 ignored issues
show
Documentation introduced by
'"' . $otherTableName . ...mplode(',', $ids) . ')' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
630
                        $sql->addInnerJoin($otherTableName, '"'.$tableName.'"."ID" = "'.$otherTableName.'"."'.$step['foreignkey'].'"');
631
                        singleton($step['class'])->extend('augmentSQL', $sql);
632
633
                        $ids = $sql->execute()->column();
634
                    }
635
636
                    if (empty($ids)) {
637
                        break;
638
                    }
639
                }
640
641
                SearchVariant::activate_state($current);
642
643
                if ($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids 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...
644
                    $base = $derivation['base'];
645
                    if (!isset($dirty[$base])) {
646
                        $dirty[$base] = array();
647
                    }
648
649
                    foreach ($ids as $rid) {
650
                        $statefulid = array('id' => $rid, 'state' => $state);
651
                        $key = serialize($statefulid);
652
                        $dirty[$base][$key] = $statefulid;
653
                    }
654
                }
655
            }
656
        }
657
658
        return $dirty;
659
    }
660
661
    /** !! These should be implemented by the full text search engine */
662
663
    abstract public function add($object);
664
    abstract public function delete($base, $id, $state);
665
666
    abstract public function commit();
667
668
    /** !! These should be implemented by the specific index */
669
670
    /**
671
     * Called during construction, this is the method that builds the structure.
672
     * Used instead of overriding __construct as we have specific execution order - code that has
673
     * to be run before _and/or_ after this.
674
     */
675
    abstract public function init();
676
}
677