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
|
|
|
use SilverStripe\ORM\SS_List; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* SearchIndex is the base index class. Each connector will provide a subclass of this that |
22
|
|
|
* provides search engine specific behavior. |
23
|
|
|
* |
24
|
|
|
* This class is responsible for: |
25
|
|
|
* |
26
|
|
|
* - Taking index calls adding classes and fields, and resolving those to value sources and types |
27
|
|
|
* |
28
|
|
|
* - Determining which records in this index need updating when a DataObject is changed |
29
|
|
|
* |
30
|
|
|
* - Providing utilities to the connector indexes |
31
|
|
|
* |
32
|
|
|
* The connector indexes are responsible for |
33
|
|
|
* |
34
|
|
|
* - Mapping types to index configuration |
35
|
|
|
* |
36
|
|
|
* - Adding and removing items to index |
37
|
|
|
* |
38
|
|
|
* - Parsing and converting SearchQueries into a form the engine will understand, and executing those queries |
39
|
|
|
* |
40
|
|
|
* The user indexes are responsible for |
41
|
|
|
* |
42
|
|
|
* - Specifying which classes and fields this index contains |
43
|
|
|
* |
44
|
|
|
* - Specifying update rules that are not extractable from metadata (because the values come from functions for instance) |
45
|
|
|
* |
46
|
|
|
*/ |
47
|
|
|
abstract class SearchIndex extends ViewableData |
48
|
|
|
{ |
49
|
|
|
/** |
50
|
|
|
* Allows this index to hide a parent index. Specifies the name of a parent index to disable |
51
|
|
|
* |
52
|
|
|
* @var string |
53
|
|
|
* @config |
54
|
|
|
*/ |
55
|
|
|
private static $hide_ancestor; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Used to separate class name and relation name in the sources array |
59
|
|
|
* this string must not be present in class name |
60
|
|
|
* @var string |
61
|
|
|
* @config |
62
|
|
|
*/ |
63
|
|
|
private static $class_delimiter = '_|_'; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* This is used to clean the source name from suffix |
67
|
|
|
* suffixes are needed to support multiple relations with the same name on different page types |
68
|
|
|
* @param string $source |
69
|
|
|
* @return string |
70
|
|
|
*/ |
71
|
|
|
protected function getSourceName($source) |
72
|
|
|
{ |
73
|
|
|
$source = explode(self::config()->get('class_delimiter'), $source); |
74
|
|
|
|
75
|
|
|
return $source[0]; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
public function __construct() |
79
|
|
|
{ |
80
|
|
|
parent::__construct(); |
81
|
|
|
$this->init(); |
82
|
|
|
|
83
|
|
|
foreach ($this->getClasses() as $class => $options) { |
84
|
|
|
SearchVariant::with($class, $options['include_children'])->call('alterDefinition', $class, $this); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
$this->buildDependancyList(); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
public function __toString() |
91
|
|
|
{ |
92
|
|
|
return 'Search Index ' . get_class($this); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Examines the classes this index is built on to try and find defined fields in the class hierarchy |
97
|
|
|
* for those classes. |
98
|
|
|
* Looks for db and viewable-data fields, although can't necessarily find type for viewable-data fields. |
99
|
|
|
* If multiple classes have a relation with the same name all of these will be included in the search index |
100
|
|
|
* Note that only classes that have the relations uninherited (defined in them) will be listed |
101
|
|
|
* this is because inherited relations do not need to be processed by index explicitly |
102
|
|
|
*/ |
103
|
|
|
public function fieldData($field, $forceType = null, $extraOptions = []) |
104
|
|
|
{ |
105
|
|
|
$fullfield = str_replace(".", "_", $field); |
106
|
|
|
$sources = $this->getClasses(); |
107
|
|
|
|
108
|
|
|
foreach ($sources as $source => $options) { |
109
|
|
|
$sources[$source]['base'] = DataObject::getSchema()->baseDataClass($source); |
110
|
|
|
$sources[$source]['lookup_chain'] = []; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$found = []; |
114
|
|
|
|
115
|
|
|
if (strpos($field, '.') !== false) { |
116
|
|
|
$lookups = explode(".", $field); |
117
|
|
|
$field = array_pop($lookups); |
118
|
|
|
|
119
|
|
|
foreach ($lookups as $lookup) { |
120
|
|
|
$next = []; |
121
|
|
|
|
122
|
|
|
foreach ($sources as $source => $baseOptions) { |
123
|
|
|
$source = $this->getSourceName($source); |
124
|
|
|
|
125
|
|
|
foreach (SearchIntrospection::hierarchy($source, $baseOptions['include_children']) as $dataclass) { |
126
|
|
|
$class = null; |
127
|
|
|
$options = $baseOptions; |
128
|
|
|
$singleton = singleton($dataclass); |
129
|
|
|
$schema = DataObject::getSchema(); |
130
|
|
|
$className = $singleton->getClassName(); |
131
|
|
|
|
132
|
|
|
if ($hasOne = $schema->hasOneComponent($className, $lookup)) { |
133
|
|
|
// we only want to include base class for relation, omit classes that inherited the relation |
134
|
|
|
$relationList = Config::inst()->get($dataclass, 'has_one', Config::UNINHERITED); |
135
|
|
|
$relationList = (!is_null($relationList)) ? $relationList : []; |
136
|
|
|
if (!array_key_exists($lookup, $relationList)) { |
137
|
|
|
continue; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$class = $hasOne; |
141
|
|
|
$options['lookup_chain'][] = array( |
142
|
|
|
'call' => 'method', 'method' => $lookup, |
143
|
|
|
'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID" |
144
|
|
|
); |
145
|
|
|
} elseif ($hasMany = $schema->hasManyComponent($className, $lookup)) { |
146
|
|
|
// we only want to include base class for relation, omit classes that inherited the relation |
147
|
|
|
$relationList = Config::inst()->get($dataclass, 'has_many', Config::UNINHERITED); |
148
|
|
|
$relationList = (!is_null($relationList)) ? $relationList : []; |
149
|
|
|
if (!array_key_exists($lookup, $relationList)) { |
150
|
|
|
continue; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$class = $hasMany; |
154
|
|
|
$options['multi_valued'] = true; |
155
|
|
|
$options['lookup_chain'][] = array( |
156
|
|
|
'call' => 'method', 'method' => $lookup, |
157
|
|
|
'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $schema->getRemoteJoinField($className, $lookup, 'has_many') |
158
|
|
|
); |
159
|
|
|
} elseif ($manyMany = $schema->manyManyComponent($className, $lookup)) { |
160
|
|
|
// we only want to include base class for relation, omit classes that inherited the relation |
161
|
|
|
$relationList = Config::inst()->get($dataclass, 'many_many', Config::UNINHERITED); |
162
|
|
|
$relationList = (!is_null($relationList)) ? $relationList : []; |
163
|
|
|
if (!array_key_exists($lookup, $relationList)) { |
164
|
|
|
continue; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
$class = $manyMany['childClass']; |
168
|
|
|
$options['multi_valued'] = true; |
169
|
|
|
$options['lookup_chain'][] = array( |
170
|
|
|
'call' => 'method', |
171
|
|
|
'method' => $lookup, |
172
|
|
|
'through' => 'many_many', |
173
|
|
|
'class' => $dataclass, |
174
|
|
|
'otherclass' => $class, |
175
|
|
|
'details' => $manyMany, |
176
|
|
|
); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
if (is_string($class) && $class) { |
180
|
|
|
if (!isset($options['origin'])) { |
181
|
|
|
$options['origin'] = $dataclass; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
// we add suffix here to prevent the relation to be overwritten by other instances |
185
|
|
|
// all sources lookups must clean the source name before reading it via getSourceName() |
186
|
|
|
$next[$class . self::config()->get('class_delimiter') . $dataclass] = $options; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
if (!$next) { |
192
|
|
|
return $next; |
193
|
|
|
} // Early out to avoid excessive empty looping |
194
|
|
|
$sources = $next; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
foreach ($sources as $class => $options) { |
199
|
|
|
$class = $this->getSourceName($class); |
200
|
|
|
$dataclasses = SearchIntrospection::hierarchy($class, $options['include_children']); |
201
|
|
|
|
202
|
|
|
while (count($dataclasses)) { |
203
|
|
|
$dataclass = array_shift($dataclasses); |
204
|
|
|
$type = null; |
205
|
|
|
$fieldoptions = $options; |
206
|
|
|
|
207
|
|
|
$fields = DataObject::getSchema()->databaseFields($class); |
208
|
|
|
|
209
|
|
|
if (isset($fields[$field])) { |
210
|
|
|
$type = $fields[$field]; |
211
|
|
|
$fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field); |
212
|
|
|
} else { |
213
|
|
|
$singleton = singleton($dataclass); |
214
|
|
|
|
215
|
|
|
if ($singleton->hasMethod("get$field") || $singleton->hasField($field)) { |
216
|
|
|
$type = $singleton->castingClass($field); |
217
|
|
|
if (!$type) { |
218
|
|
|
$type = 'String'; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
if ($singleton->hasMethod("get$field")) { |
222
|
|
|
$fieldoptions['lookup_chain'][] = array('call' => 'method', 'method' => "get$field"); |
223
|
|
|
} else { |
224
|
|
|
$fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
if ($type) { |
230
|
|
|
// Don't search through child classes of a class we matched on. TODO: Should we? |
231
|
|
|
$dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass))); |
232
|
|
|
// Trim arguments off the type string |
233
|
|
|
if (preg_match('/^(\w+)\(/', $type, $match)) { |
234
|
|
|
$type = $match[1]; |
235
|
|
|
} |
236
|
|
|
// Get the origin |
237
|
|
|
$origin = isset($fieldoptions['origin']) ? $fieldoptions['origin'] : $dataclass; |
238
|
|
|
|
239
|
|
|
$found["{$origin}_{$fullfield}"] = array( |
240
|
|
|
'name' => "{$origin}_{$fullfield}", |
241
|
|
|
'field' => $field, |
242
|
|
|
'fullfield' => $fullfield, |
243
|
|
|
'base' => $fieldoptions['base'], |
244
|
|
|
'origin' => $origin, |
245
|
|
|
'class' => $dataclass, |
246
|
|
|
'lookup_chain' => $fieldoptions['lookup_chain'], |
247
|
|
|
'type' => $forceType ? $forceType : $type, |
248
|
|
|
'multi_valued' => isset($fieldoptions['multi_valued']) ? true : false, |
249
|
|
|
'extra_options' => $extraOptions |
250
|
|
|
); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $found; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** Public, but should only be altered by variants */ |
259
|
|
|
|
260
|
|
|
protected $classes = array(); |
261
|
|
|
|
262
|
|
|
protected $fulltextFields = array(); |
263
|
|
|
|
264
|
|
|
public $filterFields = array(); |
265
|
|
|
|
266
|
|
|
protected $sortFields = array(); |
267
|
|
|
|
268
|
|
|
protected $excludedVariantStates = array(); |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Add a DataObject subclass whose instances should be included in this index |
272
|
|
|
* |
273
|
|
|
* Can only be called when addFulltextField, addFilterField, addSortField and addAllFulltextFields have not |
274
|
|
|
* yet been called for this index instance |
275
|
|
|
* |
276
|
|
|
* @throws Exception |
277
|
|
|
* @param string $class - The class to include |
278
|
|
|
* @param array $options - TODO: Remove |
279
|
|
|
*/ |
280
|
|
|
public function addClass($class, $options = array()) |
281
|
|
|
{ |
282
|
|
|
if ($this->fulltextFields || $this->filterFields || $this->sortFields) { |
|
|
|
|
283
|
|
|
throw new Exception('Can\'t add class to Index after fields have already been added'); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
$options = array_merge(array( |
287
|
|
|
'include_children' => true |
288
|
|
|
), $options); |
289
|
|
|
|
290
|
|
|
$this->classes[$class] = $options; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Get the classes added by addClass |
295
|
|
|
*/ |
296
|
|
|
public function getClasses() |
297
|
|
|
{ |
298
|
|
|
return $this->classes; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Add a field that should be fulltext searchable |
303
|
|
|
* @param string $field - The field to add |
304
|
|
|
* @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata) |
305
|
|
|
* @param string $extraOptions - Dependent on search implementation |
306
|
|
|
*/ |
307
|
|
|
public function addFulltextField($field, $forceType = null, $extraOptions = array()) |
308
|
|
|
{ |
309
|
|
|
$this->fulltextFields = array_merge($this->fulltextFields, $this->fieldData($field, $forceType, $extraOptions)); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
public function getFulltextFields() |
313
|
|
|
{ |
314
|
|
|
return $this->fulltextFields; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Add a field that should be filterable |
319
|
|
|
* @param string $field - The field to add |
320
|
|
|
* @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata) |
321
|
|
|
* @param string $extraOptions - Dependent on search implementation |
322
|
|
|
*/ |
323
|
|
|
public function addFilterField($field, $forceType = null, $extraOptions = array()) |
324
|
|
|
{ |
325
|
|
|
$this->filterFields = array_merge($this->filterFields, $this->fieldData($field, $forceType, $extraOptions)); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
public function getFilterFields() |
329
|
|
|
{ |
330
|
|
|
return $this->filterFields; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Add a field that should be sortable |
335
|
|
|
* @param string $field - The field to add |
336
|
|
|
* @param string $forceType - The type to force this field as (required in some cases, when not detectable from metadata) |
337
|
|
|
* @param string $extraOptions - Dependent on search implementation |
338
|
|
|
*/ |
339
|
|
|
public function addSortField($field, $forceType = null, $extraOptions = array()) |
340
|
|
|
{ |
341
|
|
|
$this->sortFields = array_merge($this->sortFields, $this->fieldData($field, $forceType, $extraOptions)); |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
public function getSortFields() |
345
|
|
|
{ |
346
|
|
|
return $this->sortFields; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Add all database-backed text fields as fulltext searchable fields. |
351
|
|
|
* |
352
|
|
|
* For every class included in the index, examines those classes and all subclasses looking for "Text" database |
353
|
|
|
* fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields. |
354
|
|
|
*/ |
355
|
|
|
public function addAllFulltextFields($includeSubclasses = true) |
356
|
|
|
{ |
357
|
|
|
foreach ($this->getClasses() as $class => $options) { |
358
|
|
|
$classHierarchy = SearchIntrospection::hierarchy($class, $includeSubclasses, true); |
359
|
|
|
|
360
|
|
|
foreach ($classHierarchy as $dataClass) { |
361
|
|
|
$fields = DataObject::getSchema()->databaseFields($dataClass); |
362
|
|
|
|
363
|
|
|
foreach ($fields as $field => $type) { |
364
|
|
|
list($type, $args) = ClassInfo::parse_class_spec($type); |
365
|
|
|
|
366
|
|
|
/** @var DBField $object */ |
367
|
|
|
$object = Injector::inst()->get($type, false, ['Name' => 'test']); |
368
|
|
|
if ($object instanceof DBString) { |
369
|
|
|
$this->addFulltextField($field); |
370
|
|
|
} |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Returns an interator that will let you interate through all added fields, regardless of whether they |
378
|
|
|
* were added as fulltext, filter or sort fields. |
379
|
|
|
* |
380
|
|
|
* @return MultipleArrayIterator |
381
|
|
|
*/ |
382
|
|
|
public function getFieldsIterator() |
383
|
|
|
{ |
384
|
|
|
return new MultipleArrayIterator($this->fulltextFields, $this->filterFields, $this->sortFields); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
public function excludeVariantState($state) |
388
|
|
|
{ |
389
|
|
|
$this->excludedVariantStates[] = $state; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** Returns true if some variant state should be ignored */ |
393
|
|
|
public function variantStateExcluded($state) |
394
|
|
|
{ |
395
|
|
|
foreach ($this->excludedVariantStates as $excludedstate) { |
396
|
|
|
$matches = true; |
397
|
|
|
|
398
|
|
|
foreach ($excludedstate as $variant => $variantstate) { |
399
|
|
|
if (!isset($state[$variant]) || $state[$variant] != $variantstate) { |
400
|
|
|
$matches = false; |
401
|
|
|
break; |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
if ($matches) { |
406
|
|
|
return true; |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
public $dependancyList = array(); |
412
|
|
|
|
413
|
|
|
public function buildDependancyList() |
414
|
|
|
{ |
415
|
|
|
$this->dependancyList = array_keys($this->getClasses()); |
416
|
|
|
|
417
|
|
|
foreach ($this->getFieldsIterator() as $name => $field) { |
418
|
|
|
if (!isset($field['class'])) { |
419
|
|
|
continue; |
420
|
|
|
} |
421
|
|
|
SearchIntrospection::add_unique_by_ancestor($this->dependancyList, $field['class']); |
422
|
|
|
} |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
public $derivedFields = null; |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Returns an array where each member is all the fields and the classes that are at the end of some |
429
|
|
|
* specific lookup chain from one of the base classes |
430
|
|
|
*/ |
431
|
|
|
public function getDerivedFields() |
432
|
|
|
{ |
433
|
|
|
if ($this->derivedFields === null) { |
434
|
|
|
$this->derivedFields = array(); |
435
|
|
|
|
436
|
|
|
foreach ($this->getFieldsIterator() as $name => $field) { |
437
|
|
|
if (count($field['lookup_chain']) < 2) { |
438
|
|
|
continue; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
$key = sha1($field['base'] . serialize($field['lookup_chain'])); |
442
|
|
|
$fieldname = "{$field['class']}:{$field['field']}"; |
443
|
|
|
|
444
|
|
|
if (isset($this->derivedFields[$key])) { |
445
|
|
|
$this->derivedFields[$key]['fields'][$fieldname] = $fieldname; |
446
|
|
|
SearchIntrospection::add_unique_by_ancestor($this->derivedFields['classes'], $field['class']); |
447
|
|
|
} else { |
448
|
|
|
$chain = array_reverse($field['lookup_chain']); |
449
|
|
|
array_shift($chain); |
450
|
|
|
|
451
|
|
|
$this->derivedFields[$key] = array( |
452
|
|
|
'base' => $field['base'], |
453
|
|
|
'fields' => array($fieldname => $fieldname), |
454
|
|
|
'classes' => array($field['class']), |
455
|
|
|
'chain' => $chain |
456
|
|
|
); |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
return $this->derivedFields; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Get the "document ID" (a database & variant unique id) given some "Base" class, DataObject ID and state array |
466
|
|
|
* |
467
|
|
|
* @param string $base - The base class of the object |
468
|
|
|
* @param integer $id - The ID of the object |
469
|
|
|
* @param array $state - The variant state of the object |
470
|
|
|
* @return string - The document ID as a string |
471
|
|
|
*/ |
472
|
|
|
public function getDocumentIDForState($base, $id, $state) |
473
|
|
|
{ |
474
|
|
|
ksort($state); |
475
|
|
|
$parts = array('id' => $id, 'base' => $base, 'state' => json_encode($state)); |
476
|
|
|
return implode('-', array_values($parts)); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Get the "document ID" (a database & variant unique id) given some "Base" class and DataObject |
481
|
|
|
* |
482
|
|
|
* @param DataObject $object - The object |
483
|
|
|
* @param string $base - The base class of the object |
484
|
|
|
* @param boolean $includesubs - TODO: Probably going away |
485
|
|
|
* @return string - The document ID as a string |
486
|
|
|
*/ |
487
|
|
|
public function getDocumentID($object, $base, $includesubs) |
488
|
|
|
{ |
489
|
|
|
return $this->getDocumentIDForState($base, $object->ID, SearchVariant::current_state($base, $includesubs)); |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Given an object and a field definition (as returned by fieldData) get the current value of that field on that object |
494
|
|
|
* |
495
|
|
|
* @param DataObject $object - The object to get the value from |
496
|
|
|
* @param array $field - The field definition to use |
497
|
|
|
* @return mixed - The value of the field, or null if we couldn't look it up for some reason |
498
|
|
|
*/ |
499
|
|
|
protected function _getFieldValue($object, $field) |
500
|
|
|
{ |
501
|
|
|
$errorHandler = function ($no, $str) { |
502
|
|
|
throw new Exception('HTML Parse Error: ' . $str); |
503
|
|
|
}; |
504
|
|
|
set_error_handler($errorHandler, E_ALL); |
505
|
|
|
|
506
|
|
|
try { |
507
|
|
|
foreach ($field['lookup_chain'] as $step) { |
508
|
|
|
// Just fail if we've fallen off the end of the chain |
509
|
|
|
if (!$object) { |
510
|
|
|
return null; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
// If we're looking up this step on an array or SS_List, do the step on every item, merge result |
514
|
|
|
if (is_array($object) || $object instanceof SS_List) { |
515
|
|
|
$next = array(); |
516
|
|
|
|
517
|
|
|
foreach ($object as $item) { |
518
|
|
|
if ($step['call'] == 'method') { |
519
|
|
|
$method = $step['method']; |
520
|
|
|
$item = $item->$method(); |
521
|
|
|
} else { |
522
|
|
|
$property = $step['property']; |
523
|
|
|
$item = $item->$property; |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
if ($item instanceof SS_List) { |
527
|
|
|
$next = array_merge($next, $item->toArray()); |
528
|
|
|
} elseif (is_array($item)) { |
529
|
|
|
$next = array_merge($next, $item); |
530
|
|
|
} else { |
531
|
|
|
$next[] = $item; |
532
|
|
|
} |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
$object = $next; |
536
|
|
|
} else { |
537
|
|
|
// Otherwise, just call |
538
|
|
|
if ($step['call'] == 'method') { |
539
|
|
|
$method = $step['method']; |
540
|
|
|
$object = $object->$method(); |
541
|
|
|
} elseif ($step['call'] == 'variant') { |
542
|
|
|
$variants = SearchVariant::variants(); |
543
|
|
|
$variant = $variants[$step['variant']]; |
544
|
|
|
$method = $step['method']; |
545
|
|
|
$object = $variant->$method($object); |
546
|
|
|
} else { |
547
|
|
|
$property = $step['property']; |
548
|
|
|
$object = $object->$property; |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
} |
552
|
|
|
} catch (Exception $e) { |
553
|
|
|
static::warn($e); |
554
|
|
|
$object = null; |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
restore_error_handler(); |
558
|
|
|
return $object; |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* Log non-fatal errors |
563
|
|
|
* |
564
|
|
|
* @param Exception $e |
565
|
|
|
*/ |
566
|
|
|
public static function warn($e) |
567
|
|
|
{ |
568
|
|
|
Injector::inst()->get(LoggerInterface::class)->info($e); |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
/** |
572
|
|
|
* Given a class, object id, set of stateful ids and a list of changed fields (in a special format), |
573
|
|
|
* return what statefulids need updating in this index |
574
|
|
|
* |
575
|
|
|
* Internal function used by SearchUpdater. |
576
|
|
|
* |
577
|
|
|
* @param string $class |
578
|
|
|
* @param int $id |
579
|
|
|
* @param array $statefulids |
580
|
|
|
* @param array $fields |
581
|
|
|
* @return array |
582
|
|
|
*/ |
583
|
|
|
public function getDirtyIDs($class, $id, $statefulids, $fields) |
584
|
|
|
{ |
585
|
|
|
$dirty = array(); |
586
|
|
|
|
587
|
|
|
// First, if this object is directly contained in the index, add it |
588
|
|
|
foreach ($this->classes as $searchclass => $options) { |
589
|
|
|
if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) { |
590
|
|
|
$base = DataObject::getSchema()->baseDataClass($searchclass); |
591
|
|
|
$dirty[$base] = array(); |
592
|
|
|
foreach ($statefulids as $statefulid) { |
593
|
|
|
$key = serialize($statefulid); |
594
|
|
|
$dirty[$base][$key] = $statefulid; |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
$current = SearchVariant::current_state(); |
600
|
|
|
|
601
|
|
|
|
602
|
|
|
// Then, for every derived field |
603
|
|
|
foreach ($this->getDerivedFields() as $derivation) { |
604
|
|
|
// If the this object is a subclass of any of the classes we want a field from |
605
|
|
|
if (!SearchIntrospection::is_subclass_of($class, $derivation['classes'])) { |
606
|
|
|
continue; |
607
|
|
|
} |
608
|
|
|
if (!array_intersect_key($fields, $derivation['fields'])) { |
609
|
|
|
continue; |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
foreach (SearchVariant::reindex_states($class, false) as $state) { |
613
|
|
|
SearchVariant::activate_state($state); |
614
|
|
|
|
615
|
|
|
$ids = array($id); |
616
|
|
|
|
617
|
|
|
foreach ($derivation['chain'] as $step) { |
618
|
|
|
// Use TableName for queries |
619
|
|
|
$tableName = DataObject::getSchema()->tableName($step['class']); |
|
|
|
|
620
|
|
|
|
621
|
|
|
if ($step['through'] == 'has_one') { |
622
|
|
|
$ids = DataObject::get($step['class']) |
623
|
|
|
->filter($step['foreignkey'], $ids) |
624
|
|
|
->column('ID'); |
625
|
|
|
} elseif ($step['through'] == 'has_many') { |
626
|
|
|
// foreignkey identifies a has_one column on the model linked via the has_many relation |
627
|
|
|
$ids = DataObject::get($step['otherclass']) |
628
|
|
|
->filter('ID', $ids) |
629
|
|
|
->column($step['foreignkey']); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
if (empty($ids)) { |
633
|
|
|
break; |
634
|
|
|
} |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
SearchVariant::activate_state($current); |
638
|
|
|
|
639
|
|
|
if ($ids) { |
640
|
|
|
$base = $derivation['base']; |
641
|
|
|
if (!isset($dirty[$base])) { |
642
|
|
|
$dirty[$base] = array(); |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
foreach ($ids as $rid) { |
646
|
|
|
$statefulid = array('id' => $rid, 'state' => $state); |
647
|
|
|
$key = serialize($statefulid); |
648
|
|
|
$dirty[$base][$key] = $statefulid; |
649
|
|
|
} |
650
|
|
|
} |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
return $dirty; |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** !! These should be implemented by the full text search engine */ |
658
|
|
|
|
659
|
|
|
abstract public function add($object); |
660
|
|
|
abstract public function delete($base, $id, $state); |
661
|
|
|
|
662
|
|
|
abstract public function commit(); |
663
|
|
|
|
664
|
|
|
/** !! These should be implemented by the specific index */ |
665
|
|
|
|
666
|
|
|
/** |
667
|
|
|
* Called during construction, this is the method that builds the structure. |
668
|
|
|
* Used instead of overriding __construct as we have specific execution order - code that has |
669
|
|
|
* to be run before _and/or_ after this. |
670
|
|
|
*/ |
671
|
|
|
abstract public function init(); |
672
|
|
|
} |
673
|
|
|
|
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.