Passed
Pull Request — master (#77)
by Marco
05:19 queued 03:13
created

SearchIntrospection::getRelationIntrospection()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0099

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 17
c 3
b 0
f 0
dl 0
loc 32
ccs 16
cts 17
cp 0.9412
rs 8.8333
cc 7
nc 8
nop 3
crap 7.0099
1
<?php
2
3
namespace Firesphere\SolrSearch\Helpers;
4
5
use Exception;
6
use Firesphere\SolrSearch\Indexes\BaseIndex;
7
use ReflectionException;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\DataObjectSchema;
12
use SilverStripe\ORM\DB;
13
14
/**
15
 * Some additional introspection tools that are used often by the fulltext search code
16
 */
17
class SearchIntrospection
18
{
19
    protected static $ancestry = [];
20
    protected static $hierarchy = [];
21
    /**
22
     * @var BaseIndex
23
     */
24
    protected $index;
25
    /**
26
     * @var array
27
     */
28
    protected $found = [];
29
30
    /**
31
     * Check if class is subclass of (a) the class in $instanceOf, or (b) any of the classes in the array $instanceOf
32
     * @param string $class Name of the class to test
33
     * @param array|string $instanceOf Class ancestry it should be in
34
     * @return bool
35
     * @todo remove in favour of DataObjectSchema
36
     * @static
37 1
     */
38
    public static function isSubclassOf($class, $instanceOf)
39 1
    {
40
        $ancestry = self::$ancestry[$class] ?? self::$ancestry[$class] = ClassInfo::ancestry($class);
41 1
42 1
        return is_array($instanceOf) ?
43 1
            (bool)array_intersect($instanceOf, $ancestry) :
44
            array_key_exists($instanceOf, $ancestry);
45
    }
46
47
    /**
48
     * @param $field
49
     * @return array
50
     * @throws Exception
51
     *
52 18
     */
53
    public function getFieldIntrospection($field)
54 18
    {
55 18
        $fullfield = str_replace('.', '_', $field);
56
        $sources = $this->index->getClasses();
57 18
58 18
        $schemaHelper = DataObject::getSchema();
59 18
        foreach ($sources as $source) {
60
            $sources[$source]['base'] = $schemaHelper->baseDataClass($source);
61
            $sources[$source]['lookup_chain'] = [];
62 18
        }
63
64 18
        $found = [];
65 12
66 12
        if (strpos($field, '.') !== false) {
67
            $lookups = explode('.', $field);
68 12
            $field = array_pop($lookups);
69 12
70
            foreach ($lookups as $lookup) {
71
                $next = [];
72 12
73 12
                // @todo remove repetition
74
                foreach ($sources as $source => $baseOptions) {
75
                    $next = $this->getRelationIntrospection($source, $lookup, $next);
76 12
                }
77
78
                $sources = $next;
79
            }
80 18
        }
81
82 18
        $found = $this->getFieldOptions($field, $sources, $fullfield, $found);
83
84
        return $found;
85
    }
86
87
    /**
88
     * @param $source
89
     * @param $lookup
90
     * @param array $next
91
     * @return array
92 12
     * @throws Exception
93
     */
94 12
    protected function getRelationIntrospection($source, $lookup, array $next): array
95
    {
96 12
        $source = $this->getSourceName($source);
97 12
98 12
        foreach (self::getHierarchy($source) as $dataClass) {
99 12
            $options = [];
100 12
            $singleton = singleton($dataClass);
101 12
            $schema = DataObject::getSchema();
102
            $className = $singleton->getClassName();
103 12
            $options['multi_valued'] = false;
104
105 12
            [$class, $key, $relationType] = $this->getRelationData($lookup, $schema, $className, $options);
106 12
107
            if ($relationType !== false) {
108
                if ($this->checkRelationList($dataClass, $lookup, $relationType)) {
109 12
                    continue;
110
                }
111
                $options = $this->getLookupChain($options, $lookup, $relationType, $dataClass, $class, $key);
112 12
            }
113 12
114 12
            if (is_string($class) && $class) {
115
                if (!isset($options['origin'])) {
116
                    $options['origin'] = $dataClass;
117
                }
118
119 12
                // we add suffix here to prevent the relation to be overwritten by other instances
120
                // all sources lookups must clean the source name before reading it via getSourceName()
121
                $next[$class . '_|_' . $dataClass] = $options;
122
            }
123 12
        }
124
125
        return $next;
126
    }
127
128
    /**
129
     * This is used to clean the source name from suffix
130
     * suffixes are needed to support multiple relations with the same name on different page types
131
     * @param string $source
132 13
     * @return string
133
     */
134 13
    protected function getSourceName($source)
135
    {
136 13
        $explodedSource = explode('_|_', $source);
137
138
        return $explodedSource[0];
139
    }
140
141
    /**
142
     * Get all the classes involved in a DataObject hierarchy - both super and optionally subclasses
143
     *
144
     * @static
145
     * @param string $class - The class to query
146
     * @param bool $includeSubclasses - True to return subclasses as well as super classes
147
     * @param bool $dataOnly - True to only return classes that have tables
148
     * @return array - Integer keys, String values as classes sorted by depth (most super first)
149 28
     * @throws ReflectionException
150
     */
151
    public static function getHierarchy($class, $includeSubclasses = true, $dataOnly = false): array
152
    {
153 28
        // Generate the unique key for this class and it's call type
154
        // It's a short-lived cache key for the duration of the request
155 28
        $cacheKey = sprintf('%s-%s-%s', $class, $includeSubclasses ? 'sc' : 'an', $dataOnly ? 'do' : 'al');
156 4
157 4
        if (!isset(self::$hierarchy[$cacheKey])) {
158
            $classes = array_values(ClassInfo::ancestry($class));
159 4
            $classes = self::getSubClasses($class, $includeSubclasses, $classes);
160 4
161
            $classes = array_unique($classes);
162 4
            $classes = self::excludeDataObjectIDx($classes);
163 1
164 1
            if ($dataOnly) {
165 1
                foreach ($classes as $i => $schemaClass) {
166
                    if (!DataObject::getSchema()->classHasTable($schemaClass)) {
167
                        unset($classes[$i]);
168
                    }
169
                }
170 4
            }
171
172 4
            self::$hierarchy[$cacheKey] = $classes;
173
174
            return $classes;
175 27
        }
176
177
        return self::$hierarchy[$cacheKey];
178
    }
179
180
    /**
181
     * @param $class
182
     * @param $includeSubclasses
183
     * @param array $classes
184
     * @return array
185 4
     * @throws ReflectionException
186
     */
187 4
    protected static function getSubClasses($class, $includeSubclasses, array $classes): array
188 3
    {
189 3
        if ($includeSubclasses) {
190
            $subClasses = ClassInfo::subclassesFor($class);
191
            $classes = array_merge($classes, array_values($subClasses));
192 4
        }
193
194
        return $classes;
195
    }
196
197
    /**
198
     * @param array $classes
199 4
     * @return array
200
     */
201
    protected static function excludeDataObjectIDx(array $classes): array
202 4
    {
203 4
        // Remove all classes below DataObject from the list
204 3
        $idx = array_search(DataObject::class, $classes, true);
205
        if ($idx !== false) {
206
            array_splice($classes, 0, $idx + 1);
207 4
        }
208
209
        return $classes;
210
    }
211
212
    /**
213
     * @param $lookup
214
     * @param DataObjectSchema $schema
215
     * @param $className
216
     * @param array $options
217
     * @return array
218 12
     * @throws Exception
219
     */
220 12
    protected function getRelationData($lookup, DataObjectSchema $schema, $className, array &$options): array
221 12
    {
222 12
        $class = null;
223 12
        $relationType = false;
224 12
        if ($hasOne = $schema->hasOneComponent($className, $lookup)) {
225 12
            $class = $hasOne;
226 12
            $key = $lookup . 'ID';
227 12
            $relationType = 'has_one';
228 12
        } elseif ($hasMany = $schema->hasManyComponent($className, $lookup)) {
229 12
            $class = $hasMany;
230 12
            $options['multi_valued'] = true;
231 12
            $key = $schema->getRemoteJoinField($className, $lookup);
232
            $relationType = 'has_many';
233
        } elseif ($key = $schema->manyManyComponent($className, $lookup)) {
234
            $class = $key['childClass'];
235
            $options['multi_valued'] = true;
236
            $relationType = 'many_many';
237 12
        }
238
239
        return [$class, $key, $relationType];
240
    }
241
242
    /**
243
     * @param $dataClass
244
     * @param $lookup
245
     * @param $relation
246 12
     * @return bool
247
     */
248
    public function checkRelationList($dataClass, $lookup, $relation)
249 12
    {
250 12
        // we only want to include base class for relation, omit classes that inherited the relation
251
        $relationList = Config::inst()->get($dataClass, $relation, Config::UNINHERITED);
252 12
        $relationList = $relationList ?? [];
253
254
        return (!array_key_exists($lookup, $relationList));
255
    }
256
257
    /**
258
     * @param array $options
259
     * @param string $lookup
260
     * @param string $type
261
     * @param string $dataClass
262
     * @param string $class
263
     * @param string|array $key
264 12
     * @return array
265
     */
266 12
    public function getLookupChain($options, $lookup, $type, $dataClass, $class, $key): array
267 12
    {
268 12
        $options['lookup_chain'][] = array(
269 12
            'call'       => 'method',
270 12
            'method'     => $lookup,
271 12
            'through'    => $type,
272 12
            'class'      => $dataClass,
273
            'otherclass' => $class,
274
            'foreignkey' => $key
275 12
        );
276
277
        return $options;
278
    }
279
280
    /**
281
     * @param $field
282
     * @param array $sources
283
     * @param $fullfield
284
     * @param array $found
285
     * @return array
286 18
     * @throws ReflectionException
287
     */
288 18
    protected function getFieldOptions($field, array $sources, $fullfield, array $found): array
289 18
    {
290 18
        foreach ($sources as $class => $fieldOptions) {
291 18
            if (is_int($class)) {
292
                $class = $fieldOptions;
293 18
                $fieldOptions = ['lookup_chain' => []];
294 18
            }
295
            if (!empty($this->found[$class . '_' . $field])) {
296 13
                return $this->found[$class . '_' . $field];
297 13
            }
298
            $class = $this->getSourceName($class);
299 13
            $dataclasses = self::getHierarchy($class);
300 13
301 13
            $fields = DataObject::getSchema()->databaseFields($class);
302
            while ($dataclass = array_shift($dataclasses)) {
303 13
                $type = $this->getType($fields, $field, $dataclass);
304 13
305
                if ($type) {
306 13
                    $fieldOptions = $this->getOptions($field, $fields, $fieldOptions, $dataclass);
307
                    // Don't search through child classes of a class we matched on. TODO: Should we?
308 13
                    $dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass)));
309 12
                    // Trim arguments off the type string
310
                    if (preg_match('/^(\w+)\(/', $type, $match)) {
311
                        $type = $match[1];
312 13
                    }
313 13
314 13
                    $found = $this->getFoundOriginData(
315 13
                        $field,
316 13
                        $fullfield,
317 13
                        $fieldOptions,
318 13
                        $dataclass,
319
                        $type,
320
                        $found
321
                    );
322 13
                }
323
            }
324
            $this->found[$class . '_' . $fullfield] = $found;
325
        }
326 12
327
328
        return $found;
329
    }
330
331
    /**
332
     * @param array $fields
333
     * @param string $field
334
     * @param string $dataclass
335 13
     * @return string
336
     */
337 13
    public function getType($fields, $field, $dataclass)
338 13
    {
339
        if (!empty($fields[$field])) {
340
            return $fields[$field];
341 2
        }
342 2
343 2
        $singleton = singleton($dataclass);
344
        $type = $singleton->castingClass($field);
345
        if (!$type) {
346
            $type = 'String';
347 2
        }
348
349
        return $type;
350
    }
351
352
    /**
353
     * @param $field
354
     * @param array $fields
355
     * @param array $fieldoptions
356
     * @param $dataclass
357 13
     * @return array
358
     */
359 13
    protected function getOptions($field, array $fields, array $fieldoptions, $dataclass): array
360 13
    {
361 13
        if (isset($fields[$field])) {
362 13
            $fieldoptions['lookup_chain'][] = [
363
                'call'     => 'property',
364
                'property' => $field
365 13
            ];
366
367
            return $fieldoptions;
368 2
        }
369
370 2
        $singleton = singleton($dataclass);
371
372
        if ($singleton->hasMethod("get$field")) {
373
            $fieldoptions['lookup_chain'][] = [
374
                'call'   => 'method',
375
                'method' => "get$field"
376
            ];
377 2
        }
378
379
        return $fieldoptions;
380
    }
381
382
    /**
383
     * @param string $field
384
     * @param string $fullfield
385
     * @param array $fieldOptions
386
     * @param string $dataclass
387
     * @param string $type
388
     * @param array $found
389 13
     * @return array
390
     */
391
    protected function getFoundOriginData($field, $fullfield, $fieldOptions, $dataclass, $type, $found): array
392 13
    {
393
        // Get the origin
394 13
        $origin = $fieldOptions['origin'] ?? $dataclass;
395 13
396 13
        $found["{$origin}_{$fullfield}"] = [
397 13
            'name'         => "{$origin}_{$fullfield}",
398 13
            'field'        => $field,
399 13
            'fullfield'    => $fullfield,
400 13
            'origin'       => $origin,
401 13
            'class'        => $dataclass,
402 13
            'lookup_chain' => $fieldOptions['lookup_chain'],
403
            'type'         => $type,
404
            'multi_valued' => isset($fieldOptions['multi_valued']) ? true : false,
405 13
        ];
406
407
        return $found;
408
    }
409
410
    /**
411 1
     * @return BaseIndex
412
     */
413 1
    public function getIndex(): BaseIndex
414
    {
415
        return $this->index;
416
    }
417
418
    /**
419
     * @param mixed $index
420 25
     * @return $this
421
     */
422 25
    public function setIndex($index)
423
    {
424 25
        $this->index = $index;
425
426
        return $this;
427
    }
428
429
    /**
430 1
     * @return array
431
     */
432 1
    public function getFound(): array
433
    {
434
        return $this->found;
435
    }
436
}
437