Passed
Push — hans/core-extraction ( c9ddfc...71713a )
by Simon
07:14 queued 02:17
created

FieldResolver::getNext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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