Passed
Push — master ( e8ff83...4bcbf7 )
by Simon
01:51
created

SearchIntrospection::add_unique_by_ancestor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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