Passed
Push — sheepy/introspection ( 17b395...901708 )
by Simon
08:33 queued 05:46
created

SearchIntrospection   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 419
Duplicated Lines 0 %

Test Coverage

Coverage 93.9%

Importance

Changes 18
Bugs 0 Features 0
Metric Value
wmc 50
eloc 152
c 18
b 0
f 0
dl 0
loc 419
ccs 154
cts 164
cp 0.939
rs 8.4

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isSubclassOf() 0 7 2
A excludeDataObjectIDx() 0 9 2
A getFound() 0 3 1
A getType() 0 13 3
A getLookupChain() 0 12 1
A getFoundOriginData() 0 17 2
A getSubClasses() 0 8 2
A checkRelationList() 0 7 1
A getSourceName() 0 5 1
B getRelationIntrospection() 0 32 7
A getIndex() 0 3 1
A setIndex() 0 5 1
A getRelationData() 0 20 4
B getHierarchy() 0 27 7
A getOptions() 0 21 3
B getFieldOptions() 0 41 7
A getFieldIntrospection() 0 33 5

How to fix   Complexity   

Complex Class

Complex classes like SearchIntrospection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SearchIntrospection, and based on these observations, apply Extract Interface, too.

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