Passed
Push — sheepy/introspection ( bffe5e...776111 )
by Simon
05:12
created

SearchIntrospection::getRelationIntrospection()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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