Completed
Push — master ( 44f7bc...3e4910 )
by
unknown
12s
created

SearchVariant::clear_variant_cache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Variants;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\FullTextSearch\Utils\CombinationsArrayIterator;
9
use ReflectionClass;
10
11
/**
12
 * A Search Variant handles decorators and other situations where the items to reindex or search through are modified
13
 * from the default state - for instance, dealing with Versioned or Subsite
14
 */
15
abstract class SearchVariant
16
{
17
    use Configurable;
18
19
    /**
20
     * Whether this variant is enabled
21
     *
22
     * @config
23
     * @var boolean
24
     */
25
    private static $enabled = true;
26
27
    public function __construct()
28
    {
29
    }
30
31
    /*** OVERRIDES start here */
32
33
    /**
34
     * Variants can provide any functions they want, but they _must_ override these functions
35
     * with specific ones
36
     */
37
38
    /**
39
     * Return false if there is something missing from the environment (probably a
40
     * not installed module) that means this variant can't apply to any class
41
     */
42
    public function appliesToEnvironment()
43
    {
44
        return $this->config()->get('enabled');
45
    }
46
47
    /**
48
     * Return true if this variant applies to the passed class & subclass
49
     */
50
    abstract public function appliesTo($class, $includeSubclasses);
51
52
    /**
53
     * Return the current state
54
     */
55
    abstract public function currentState();
56
    /**
57
     * Return all states to step through to reindex all items
58
     */
59
    abstract public function reindexStates();
60
    /**
61
     * Activate the passed state
62
     */
63
    abstract public function activateState($state);
64
65
    /**
66
     * Apply this variant to a search query
67
     *
68
     * @param SearchQuery $query
69
     * @param SearchIndex $index
70
     */
71
    abstract public function alterQuery($query, $index);
72
73
    /*** OVERRIDES end here*/
74
75
    /** Holds a cache of all variants */
76
    protected static $variants = null;
77
    /** Holds a cache of the variants keyed by "class!" "1"? (1 = include subclasses) */
78
    protected static $class_variants = array();
79
80
    /**
81
     * Returns an array of variants.
82
     *
83
     * With no arguments, returns all variants
84
     *
85
     * With a classname as the first argument, returns the variants that apply to that class
86
     * (optionally including subclasses)
87
     *
88
     * @static
89
     * @param string $class - The class name to get variants for
90
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
91
     * @return array - An array of (string)$variantClassName => (Object)$variantInstance pairs
92
     */
93
    public static function variants($class = null, $includeSubclasses = true)
94
    {
95
        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...
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
     * A shortcut to with when calling without passing in a class,
171
     *
172
     * SearchVariant::call(...) ==== SearchVariant::with()->call(...);
173
     */
174
175
    public static function call($method, &...$args)
176
    {
177
        return self::with()->call($method, ...$args);
178
    }
179
180
    /**
181
     * Get the current state of every variant
182
     * @static
183
     * @return array
184
     */
185
    public static function current_state($class = null, $includeSubclasses = true)
186
    {
187
        $state = array();
188
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
189
            $state[$variant] = $instance->currentState();
190
        }
191
        return $state;
192
    }
193
194
    /**
195
     * Activate all the states in the passed argument
196
     * @static
197
     * @param array $state A set of (string)$variantClass => (any)$state pairs , e.g. as returned by
198
     * SearchVariant::current_state()
199
     * @return void
200
     */
201
    public static function activate_state($state)
202
    {
203
        foreach (self::variants() as $variant => $instance) {
204
            if (isset($state[$variant]) && $instance->appliesToEnvironment()) {
205
                $instance->activateState($state[$variant]);
206
            }
207
        }
208
    }
209
210
    /**
211
     * Return an iterator that, when used in a for loop, activates one combination of reindex states per loop, and restores
212
     * back to the original state at the end
213
     * @static
214
     * @param string $class - The class name to get variants for
215
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
216
     * @return SearchVariant_ReindexStateIteratorRet - The iterator to foreach loop over
217
     */
218
    public static function reindex_states($class = null, $includeSubclasses = true)
219
    {
220
        $allstates = array();
221
222
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
223
            if ($states = $instance->reindexStates()) {
224
                $allstates[$variant] = $states;
225
            }
226
        }
227
228
        return $allstates ? new CombinationsArrayIterator($allstates) : array(array());
229
    }
230
231
232
    /**
233
     * Add new filter field to index safely.
234
     *
235
     * This method will respect existing filters with the same field name that
236
     * correspond to multiple classes
237
     *
238
     * @param SearchIndex $index
239
     * @param string $name Field name
240
     * @param array $field Field spec
241
     */
242
    protected function addFilterField($index, $name, $field)
243
    {
244
        // If field already exists, make sure to merge origin / base fields
245
        if (isset($index->filterFields[$name])) {
246
            $field['base'] = $this->mergeClasses(
247
                $index->filterFields[$name]['base'],
248
                $field['base']
249
            );
250
            $field['origin'] = $this->mergeClasses(
251
                $index->filterFields[$name]['origin'],
252
                $field['origin']
253
            );
254
        }
255
256
        $index->filterFields[$name] = $field;
257
    }
258
259
    /**
260
     * Merge sets of (or individual) class names together for a search index field.
261
     *
262
     * If there is only one unique class name, then just return it as a string instead of array.
263
     *
264
     * @param array|string $left Left class(es)
265
     * @param array|string $right Right class(es)
266
     * @return array|string List of classes, or single class
267
     */
268
    protected function mergeClasses($left, $right)
269
    {
270
        // Merge together and remove dupes
271
        if (!is_array($left)) {
272
            $left = array($left);
273
        }
274
        if (!is_array($right)) {
275
            $right = array($right);
276
        }
277
        $merged = array_values(array_unique(array_merge($left, $right)));
278
279
        // If there is only one item, return it as a single string
280
        if (count($merged) === 1) {
281
            return reset($merged);
282
        }
283
        return $merged;
284
    }
285
}
286