Passed
Push — master ( 4a8dc1...e8ff83 )
by Simon
01:52
created

SearchIntrospection::getHierarchy()   B

Complexity

Conditions 9
Paths 36

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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