Passed
Push — master ( dd7df5...96c543 )
by Simon
01:32
created

SearchIntrospection::isSubclassOf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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