Passed
Push — master ( 4bcbf7...dd7df5 )
by Simon
01:32
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 Method

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