Completed
Push — master ( 415fde...48f3df )
by
unknown
11s
created

SearchVariant::withCommon()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 1
1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Variants;
4
5
use ReflectionClass;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\FullTextSearch\Utils\CombinationsArrayIterator;
9
10
/**
11
 * A Search Variant handles decorators and other situations where the items to reindex or search through are modified
12
 * from the default state - for instance, dealing with Versioned or Subsite
13
 */
14
abstract class SearchVariant
15
{
16
    use Configurable;
17
18
    /**
19
     * Whether this variant is enabled
20
     *
21
     * @config
22
     * @var boolean
23
     */
24
    private static $enabled = true;
25
26
    public function __construct()
27
    {
28
    }
29
30
    /*** OVERRIDES start here */
31
32
    /**
33
     * Variants can provide any functions they want, but they _must_ override these functions
34
     * with specific ones
35
     */
36
37
    /**
38
     * Return false if there is something missing from the environment (probably a
39
     * not installed module) that means this variant can't apply to any class
40
     */
41
    public function appliesToEnvironment()
42
    {
43
        return $this->config()->get('enabled');
44
    }
45
46
    /**
47
     * Return true if this variant applies to the passed class & subclass
48
     */
49
    abstract public function appliesTo($class, $includeSubclasses);
50
51
    /**
52
     * Return the current state
53
     */
54
    abstract public function currentState();
55
    /**
56
     * Return all states to step through to reindex all items
57
     */
58
    abstract public function reindexStates();
59
    /**
60
     * Activate the passed state
61
     */
62
    abstract public function activateState($state);
63
64
    /**
65
     * Apply this variant to a search query
66
     *
67
     * @param SearchQuery $query
68
     * @param SearchIndex $index
69
     */
70
    abstract public function alterQuery($query, $index);
71
72
    /*** OVERRIDES end here*/
73
74
    /** Holds a cache of all variants */
75
    protected static $variants = null;
76
    /** Holds a cache of the variants keyed by "class!" "1"? (1 = include subclasses) */
77
    protected static $class_variants = array();
78
79
    /**
80
     * Returns an array of variants.
81
     *
82
     * With no arguments, returns all variants
83
     *
84
     * With a classname as the first argument, returns the variants that apply to that class
85
     * (optionally including subclasses)
86
     *
87
     * @static
88
     * @param string $class - The class name to get variants for
89
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
90
     * @return array - An array of (string)$variantClassName => (Object)$variantInstance pairs
91
     */
92
    public static function variants($class = null, $includeSubclasses = true)
93
    {
94
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
95
            // Build up and cache a list of all search variants (subclasses of SearchVariant)
96
            if (self::$variants === null) {
97
                $classes = ClassInfo::subclassesFor(static::class);
98
99
                $concrete = array();
100
                foreach ($classes as $variantclass) {
101
                    $ref = new ReflectionClass($variantclass);
102
                    if ($ref->isInstantiable()) {
103
                        $variant = singleton($variantclass);
104
                        if ($variant->appliesToEnvironment()) {
105
                            $concrete[$variantclass] = $variant;
106
                        }
107
                    }
108
                }
109
110
                self::$variants = $concrete;
111
            }
112
113
            return self::$variants;
114
        } else {
115
            $key = $class . '!' . $includeSubclasses;
116
117
            if (!isset(self::$class_variants[$key])) {
118
                self::$class_variants[$key] = array();
119
120
                foreach (self::variants() as $variantclass => $instance) {
121
                    if ($instance->appliesTo($class, $includeSubclasses)) {
122
                        self::$class_variants[$key][$variantclass] = $instance;
123
                    }
124
                }
125
            }
126
127
            return self::$class_variants[$key];
128
        }
129
    }
130
131
    /**
132
     * Clear the cached variants
133
     */
134
    public static function clear_variant_cache()
135
    {
136
        self::$class_variants = [];
137
    }
138
139
    /** Holds a cache of SearchVariant_Caller instances, one for each class/includeSubclasses setting */
140
    protected static $call_instances = array();
141
142
    /**
143
     * Lets you call any function on all variants that support it, in the same manner as "Object#extend" calls
144
     * a method from extensions.
145
     *
146
     * Usage: SearchVariant::with(...)->call($method, $arg1, ...);
147
     *
148
     * @static
149
     *
150
     * @param string $class - (Optional) a classname. If passed, only variants that apply to that class will be checked / called
151
     *
152
     * @param bool $includeSubclasses - (Optional) If false, only variants that apply strictly to the passed class or its super-classes
153
     * will be checked. If true (the default), variants that apply to any sub-class of the passed class with also be checked
154
     *
155
     * @return SearchVariant_Caller An object with one method, call()
156
     */
157
    public static function with($class = null, $includeSubclasses = true)
158
    {
159
        // Make the cache key
160
        $key = $class ? $class . '!' . $includeSubclasses : '!';
161
        // If no SearchVariant_Caller instance yet, create it
162
        if (!isset(self::$call_instances[$key])) {
163
            self::$call_instances[$key] = new SearchVariant_Caller(self::variants($class, $includeSubclasses));
164
        }
165
        // Then return it
166
        return self::$call_instances[$key];
167
    }
168
169
    /**
170
     * Similar to {@link SearchVariant::with}, except will only use variants that apply to at least one of the classes
171
     * in the input array, where {@link SearchVariant::with} will run the query on the specific class you give it.
172
     *
173
     * @param string[] $classes
174
     * @return SearchVariant_Caller
175
     */
176
    public static function withCommon(array $classes = [])
177
    {
178
        // Allow caching
179
        $cacheKey = sha1(serialize($classes));
180
        if (isset(self::$call_instances[$cacheKey])) {
181
            return self::$call_instances[$cacheKey];
182
        }
183
184
        // Construct new array of variants applicable to at least one class in the list
185
        $commonVariants = [];
186
        foreach ($classes as $class => $options) {
187
            // Extract relevant class options
188
            $includeSubclasses = isset($options['include_children']) ? $options['include_children'] : true;
189
190
            // Get the variants for the current class
191
            $variantsForClass = self::variants($class, $includeSubclasses);
0 ignored issues
show
Bug introduced by
It seems like $includeSubclasses defined by isset($options['include_...clude_children'] : true on line 188 can also be of type string; however, SilverStripe\FullTextSea...archVariant::variants() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
192
193
            // Merge the variants applicable to the current class into the list of common variants, using
194
            // the variant instance to replace any previous versions for the same class name (should be singleton
195
            // anyway).
196
            $commonVariants = array_replace($commonVariants, $variantsForClass);
197
        }
198
199
        // Cache for future calls
200
        self::$call_instances[$cacheKey] = new SearchVariant_Caller($commonVariants);
201
202
        return self::$call_instances[$cacheKey];
203
    }
204
205
    /**
206
     * A shortcut to with when calling without passing in a class,
207
     *
208
     * SearchVariant::call(...) ==== SearchVariant::with()->call(...);
209
     */
210
211
    public static function call($method, &...$args)
212
    {
213
        return self::with()->call($method, ...$args);
214
    }
215
216
    /**
217
     * Get the current state of every variant
218
     * @static
219
     * @return array
220
     */
221
    public static function current_state($class = null, $includeSubclasses = true)
222
    {
223
        $state = array();
224
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
225
            $state[$variant] = $instance->currentState();
226
        }
227
        return $state;
228
    }
229
230
    /**
231
     * Activate all the states in the passed argument
232
     * @static
233
     * @param array $state A set of (string)$variantClass => (any)$state pairs , e.g. as returned by
234
     * SearchVariant::current_state()
235
     * @return void
236
     */
237
    public static function activate_state($state)
238
    {
239
        foreach (self::variants() as $variant => $instance) {
240
            if (isset($state[$variant]) && $instance->appliesToEnvironment()) {
241
                $instance->activateState($state[$variant]);
242
            }
243
        }
244
    }
245
246
    /**
247
     * Return an iterator that, when used in a for loop, activates one combination of reindex states per loop, and restores
248
     * back to the original state at the end
249
     * @static
250
     * @param string $class - The class name to get variants for
251
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
252
     * @return SearchVariant_ReindexStateIteratorRet - The iterator to foreach loop over
253
     */
254
    public static function reindex_states($class = null, $includeSubclasses = true)
255
    {
256
        $allstates = array();
257
258
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
259
            if ($states = $instance->reindexStates()) {
260
                $allstates[$variant] = $states;
261
            }
262
        }
263
264
        return $allstates ? new CombinationsArrayIterator($allstates) : array(array());
265
    }
266
267
268
    /**
269
     * Add new filter field to index safely.
270
     *
271
     * This method will respect existing filters with the same field name that
272
     * correspond to multiple classes
273
     *
274
     * @param SearchIndex $index
275
     * @param string $name Field name
276
     * @param array $field Field spec
277
     */
278
    protected function addFilterField($index, $name, $field)
279
    {
280
        // If field already exists, make sure to merge origin / base fields
281
        if (isset($index->filterFields[$name])) {
282
            $field['base'] = $this->mergeClasses(
283
                $index->filterFields[$name]['base'],
284
                $field['base']
285
            );
286
            $field['origin'] = $this->mergeClasses(
287
                $index->filterFields[$name]['origin'],
288
                $field['origin']
289
            );
290
        }
291
292
        $index->filterFields[$name] = $field;
293
    }
294
295
    /**
296
     * Merge sets of (or individual) class names together for a search index field.
297
     *
298
     * If there is only one unique class name, then just return it as a string instead of array.
299
     *
300
     * @param array|string $left Left class(es)
301
     * @param array|string $right Right class(es)
302
     * @return array|string List of classes, or single class
303
     */
304
    protected function mergeClasses($left, $right)
305
    {
306
        // Merge together and remove dupes
307
        if (!is_array($left)) {
308
            $left = array($left);
309
        }
310
        if (!is_array($right)) {
311
            $right = array($right);
312
        }
313
        $merged = array_values(array_unique(array_merge($left, $right)));
314
315
        // If there is only one item, return it as a single string
316
        if (count($merged) === 1) {
317
            return reset($merged);
318
        }
319
        return $merged;
320
    }
321
}
322