Passed
Push — hans/logtests ( 76c086...71695d )
by Simon
06:24 queued 02:27
created

FieldResolver::getSubClasses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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