Passed
Push — hans/code-cleanup ( 7f6349...96a9e1 )
by Simon
06:38
created

FieldResolver   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Test Coverage

Coverage 97.35%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 102
dl 0
loc 366
ccs 110
cts 113
cp 0.9735
rs 9.2
c 2
b 0
f 0
wmc 40

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getBuildSources() 0 11 2
A getSourceName() 0 5 1
A getNext() 0 12 2
A getFoundOriginData() 0 22 2
A getFieldOptions() 0 24 5
A getType() 0 17 3
A isSubclassOf() 0 9 2
A getHierarchy() 0 21 5
A getHierarchyClasses() 0 13 2
A resolveRelation() 0 22 5
A getRelationData() 0 14 4
A excludeDataObjectIDx() 0 9 2
A resolveField() 0 20 3
A getSubClasses() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like FieldResolver 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 FieldResolver, 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\Traits\GetSetSearchResolverTrait;
7
use ReflectionException;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DataObjectSchema;
11
12
/**
13
 * Class FieldResolver
14
 * Some additional introspection tools that are used often by the fulltext search code
15
 *
16
 * @package Firesphere\SolrSearch\Helpers
17
 */
18
class FieldResolver
19
{
20
    use GetSetSearchResolverTrait;
21
    /**
22
     * @var array Class Ancestry
23
     */
24
    protected static $ancestry = [];
25
    /**
26
     * @var array Class Hierarchy, could be replaced with Ancestry
27
     */
28
    protected static $hierarchy = [];
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
     *
33
     * @param string $class Name of the class to test
34
     * @param array|string $instanceOf Class ancestry it should be in
35
     * @return bool
36
     * @static
37
     */
38 1
    public static function isSubclassOf($class, $instanceOf): bool
39
    {
40 1
        $ancestry = self::$ancestry[$class] ?? self::$ancestry[$class] = ClassInfo::ancestry($class);
41
42 1
        if (is_array($instanceOf)) {
43 1
            return (bool)array_intersect($instanceOf, $ancestry);
44
        }
45
46
        return array_key_exists($instanceOf, $ancestry);
47
    }
48
49
    /**
50
     * Resolve a field ancestry
51
     *
52
     * @param $field
53
     * @return array
54
     * @throws Exception
55
     *
56
     */
57 38
    public function resolveField($field)
58
    {
59 38
        $fullfield = str_replace('.', '_', $field);
60
61 38
        $buildSources = $this->getBuildSources();
62
63 38
        $found = [];
64
65 38
        if (strpos($field, '.') !== false) {
66 38
            $lookups = explode('.', $field);
67 38
            $field = array_pop($lookups);
68
69 38
            foreach ($lookups as $lookup) {
70 38
                $buildSources = $this->getNext($buildSources, $lookup);
71
            }
72
        }
73
74 38
        $found = $this->getFieldOptions($field, $buildSources, $fullfield, $found);
75
76 38
        return $found;
77
    }
78
79
    /**
80
     * Get the sources to build in to a Solr field
81
     *
82
     * @return array
83
     */
84 38
    protected function getBuildSources(): array
85
    {
86 38
        $sources = $this->index->getClasses();
87 38
        $buildSources = [];
88
89 38
        $schemaHelper = DataObject::getSchema();
90 38
        foreach ($sources as $source) {
91 38
            $buildSources[$source]['base'] = $schemaHelper->baseDataClass($source);
92
        }
93
94 38
        return $buildSources;
95
    }
96
97
    /**
98
     * Get the next lookup item from the buildSources
99
     *
100
     * @param array $buildSources
101
     * @param $lookup
102
     * @return array
103
     * @throws Exception
104
     */
105 38
    protected function getNext(array $buildSources, $lookup): array
106
    {
107 38
        $next = [];
108
109
        // @todo remove repetition
110 38
        foreach ($buildSources as $source => $baseOptions) {
111 38
            $next = $this->resolveRelation($source, $lookup, $next, $baseOptions);
112
        }
113
114 38
        $buildSources = $next;
115
116 38
        return $buildSources;
117
    }
118
119
    /**
120
     * Resolve relations if possible
121
     *
122
     * @param string $source
123
     * @param $lookup
124
     * @param array $next
125
     * @param array $options
126
     * @return array
127
     * @throws ReflectionException
128
     * @throws Exception
129
     */
130 38
    protected function resolveRelation($source, $lookup, array $next, array &$options): array
131
    {
132 38
        $source = $this->getSourceName($source);
133
134 38
        foreach (self::getHierarchy($source) as $dataClass) {
135 38
            $schema = DataObject::getSchema();
136 38
            $options['multi_valued'] = false;
137
138 38
            $class = $this->getRelationData($lookup, $schema, $dataClass, $options);
139
140 38
            if (is_string($class) && $class) {
141 38
                if (!isset($options['origin'])) {
142 38
                    $options['origin'] = $source;
143
                }
144
145
                // we add suffix here to prevent the relation to be overwritten by other instances
146
                // all sources lookups must clean the source name before reading it via getSourceName()
147 38
                $next[$class . '|xkcd|' . $dataClass] = $options;
148
            }
149
        }
150
151 38
        return $next;
152
    }
153
154
    /**
155
     * This is used to clean the source name from suffix
156
     * suffixes are needed to support multiple relations with the same name on different page types
157
     *
158
     * @param string $source
159
     * @return string
160
     */
161 38
    private function getSourceName($source)
162
    {
163 38
        $explodedSource = explode('|xkcd|', $source);
164
165 38
        return $explodedSource[0];
166
    }
167
168
    /**
169
     * Get all the classes involved in a DataObject hierarchy - both super and optionally subclasses
170
     *
171
     * @static
172
     * @param string $class - The class to query
173
     * @param bool $includeSubclasses - True to return subclasses as well as super classes
174
     * @param bool $dataOnly - True to only return classes that have tables
175
     * @return array - Integer keys, String values as classes sorted by depth (most super first)
176
     * @throws ReflectionException
177
     */
178 80
    public static function getHierarchy($class, $includeSubclasses = true, $dataOnly = false): array
179
    {
180
        // Generate the unique key for this class and it's call type
181
        // It's a short-lived cache key for the duration of the request
182 80
        $cacheKey = sprintf('%s-%s-%s', $class, $includeSubclasses ? 'sc' : 'an', $dataOnly ? 'do' : 'al');
183
184 80
        if (!isset(self::$hierarchy[$cacheKey])) {
185 5
            $classes = self::getHierarchyClasses($class, $includeSubclasses);
186
187 5
            if ($dataOnly) {
188 1
                $classes = array_filter($classes, static function ($class) {
189 1
                    return DataObject::getSchema()->classHasTable($class);
190 1
                });
191
            }
192
193 5
            self::$hierarchy[$cacheKey] = array_values($classes);
194
195 5
            return array_values($classes);
196
        }
197
198 80
        return self::$hierarchy[$cacheKey];
199
    }
200
201
    /**
202
     * Get the hierarchy for a class
203
     *
204
     * @param $class
205
     * @param $includeSubclasses
206
     * @return array
207
     * @throws ReflectionException
208
     * @todo clean this up to be more compatible with PHP features
209
     */
210 5
    protected static function getHierarchyClasses($class, $includeSubclasses): array
211
    {
212 5
        if (!isset(self::$ancestry[$class])) {
213 4
            self::$ancestry[$class] = array_values(ClassInfo::ancestry($class));
214
        }
215 5
        $ancestry = self::$ancestry[$class];
216
217 5
        $classes = self::getSubClasses($class, $includeSubclasses, $ancestry);
218
219 5
        $classes = array_unique($classes);
220 5
        $classes = self::excludeDataObjectIDx($classes);
221
222 5
        return $classes;
223
    }
224
225
    /**
226
     * Get the subclasses for the given class
227
     * Should be replaced with PHP native methods
228
     *
229
     * @param $class
230
     * @param $includeSubclasses
231
     * @param array $classes
232
     * @return array
233
     * @throws ReflectionException
234
     */
235 5
    private static function getSubClasses($class, $includeSubclasses, array $classes): array
236
    {
237 5
        if ($includeSubclasses) {
238 4
            $subClasses = ClassInfo::subclassesFor($class);
239 4
            $classes = array_merge($classes, array_values($subClasses));
240
        }
241
242 5
        return $classes;
243
    }
244
245
    /**
246
     * Objects to exclude from the index
247
     *
248
     * @param array $classes
249
     * @return array
250
     */
251 5
    private static function excludeDataObjectIDx(array $classes): array
252
    {
253
        // Remove all classes below DataObject from the list
254 5
        $idx = array_search(DataObject::class, $classes, true);
255 5
        if ($idx !== false) {
256 5
            array_splice($classes, 0, $idx + 1);
257
        }
258
259 5
        return $classes;
260
    }
261
262
    /**
263
     * Relational data
264
     *
265
     * @param $lookup
266
     * @param DataObjectSchema $schema
267
     * @param $className
268
     * @param array $options
269
     * @return string|array|null
270
     * @throws Exception
271
     */
272 38
    protected function getRelationData($lookup, DataObjectSchema $schema, $className, array &$options)
273
    {
274 38
        if ($hasOne = $schema->hasOneComponent($className, $lookup)) {
275 38
            return $hasOne;
276
        }
277 38
        $options['multi_valued'] = true;
278 38
        if ($hasMany = $schema->hasManyComponent($className, $lookup)) {
279 38
            return $hasMany;
280
        }
281 38
        if ($key = $schema->manyManyComponent($className, $lookup)) {
282
            return $key['childClass'];
283
        }
284
285 38
        return null;
286
    }
287
288
    /**
289
     * Create field options for the given index field
290
     *
291
     * @param $field
292
     * @param array $sources
293
     * @param string $fullfield
294
     * @param array $found
295
     * @return array
296
     * @throws ReflectionException
297
     */
298 38
    protected function getFieldOptions($field, array $sources, $fullfield, array $found): array
299
    {
300 38
        foreach ($sources as $class => $fieldOptions) {
301 38
            $class = $this->getSourceName($class);
302 38
            $dataclasses = self::getHierarchy($class);
303
304 38
            $fields = DataObject::getSchema()->databaseFields($class);
305 38
            while ($dataclass = array_shift($dataclasses)) {
306 38
                $type = $this->getType($fields, $field, $dataclass);
307
308 38
                if ($type) {
309
                    // Don't search through child classes of a class we matched on.
310 38
                    $dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass)));
311
                    // Trim arguments off the type string
312 38
                    if (preg_match('/^(\w+)\(/', $type, $match)) {
313 38
                        $type = $match[1];
314
                    }
315
316 38
                    $found = $this->getFoundOriginData($field, $fullfield, $fieldOptions, $dataclass, $type, $found);
317
                }
318
            }
319
        }
320
321 38
        return $found;
322
    }
323
324
    /**
325
     * Get the type of this field
326
     *
327
     * @param array $fields
328
     * @param string $field
329
     * @param string $dataclass
330
     * @return string
331
     */
332 38
    protected function getType($fields, $field, $dataclass): string
333
    {
334 38
        if (!empty($fields[$field])) {
335 38
            return $fields[$field];
336
        }
337
338
        /** @var DataObject $singleton */
339 37
        $singleton = singleton($dataclass);
340
341 37
        $type = $singleton->castingClass($field);
342
343 37
        if (!$type) {
344
            // @todo should this be null?
345
            $type = 'String';
346
        }
347
348 37
        return $type;
349
    }
350
351
    /**
352
     * FoundOriginData is a helper to make sure the options are properly set.
353
     *
354
     * @param string $field
355
     * @param string $fullField
356
     * @param array $fieldOptions
357
     * @param string $dataclass
358
     * @param string $type
359
     * @param array $found
360
     * @return array
361
     */
362 38
    private function getFoundOriginData(
363
        $field,
364
        $fullField,
365
        $fieldOptions,
366
        $dataclass,
367
        $type,
368
        $found
369
    ): array {
370
        // Get the origin
371 38
        $origin = $fieldOptions['origin'] ?? $dataclass;
372
373 38
        $found["{$origin}_{$fullField}"] = [
374 38
            'name'         => "{$origin}_{$fullField}",
375 38
            'field'        => $field,
376 38
            'fullfield'    => $fullField,
377 38
            'origin'       => $origin,
378 38
            'class'        => $dataclass,
379 38
            'type'         => $type,
380 38
            'multi_valued' => isset($fieldOptions['multi_valued']) ? true : false,
381
        ];
382
383 38
        return $found;
384
    }
385
}
386