Issues (186)

src/Search/Variants/SearchVariant.php (2 issues)

1
<?php
2
3
namespace SilverStripe\FullTextSearch\Search\Variants;
4
5
use ReflectionClass;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\Core\Extensible;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex;
10
use SilverStripe\FullTextSearch\Search\Queries\SearchQuery;
11
use SilverStripe\FullTextSearch\Utils\CombinationsArrayIterator;
12
13
/**
14
 * A Search Variant handles decorators and other situations where the items to reindex or search through are modified
15
 * from the default state - for instance, dealing with Versioned or Subsite
16
 */
17
abstract class SearchVariant
18
{
19
    use Configurable;
20
    use Extensible;
21
22
    /**
23
     * Whether this variant is enabled
24
     *
25
     * @config
26
     * @var boolean
27
     */
28
    private static $enabled = true;
29
30
    public function __construct()
31
    {
32
    }
33
34
    /*** OVERRIDES start here */
35
36
    /**
37
     * Variants can provide any functions they want, but they _must_ override these functions
38
     * with specific ones
39
     */
40
41
    /**
42
     * Return false if there is something missing from the environment (probably a
43
     * not installed module) that means this variant can't apply to any class
44
     */
45
    public function appliesToEnvironment()
46
    {
47
        return $this->config()->get('enabled');
48
    }
49
50
    /**
51
     * Return true if this variant applies to the passed class & subclass
52
     */
53
    abstract public function appliesTo($class, $includeSubclasses);
54
55
    /**
56
     * Return the current state
57
     */
58
    abstract public function currentState();
59
    /**
60
     * Return all states to step through to reindex all items
61
     */
62
    abstract public function reindexStates();
63
    /**
64
     * Activate the passed state
65
     */
66
    abstract public function activateState($state);
67
68
    /**
69
     * Apply this variant to a search query
70
     *
71
     * @param SearchQuery $query
72
     * @param SearchIndex $index
73
     */
74
    abstract public function alterQuery($query, $index);
75
76
    /*** OVERRIDES end here*/
77
78
    /** Holds a cache of all variants */
79
    protected static $variants = null;
80
    /** Holds a cache of the variants keyed by "class!" "1"? (1 = include subclasses) */
81
    protected static $class_variants = array();
82
83
    /**
84
     * Returns an array of variants.
85
     *
86
     * With no arguments, returns all variants
87
     *
88
     * With a classname as the first argument, returns the variants that apply to that class
89
     * (optionally including subclasses)
90
     *
91
     * @static
92
     * @param string $class - The class name to get variants for
93
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
94
     * @return array - An array of (string)$variantClassName => (Object)$variantInstance pairs
95
     */
96
    public static function variants($class = null, $includeSubclasses = true)
97
    {
98
        if (!$class) {
99
            // Build up and cache a list of all search variants (subclasses of SearchVariant)
100
            if (self::$variants === null) {
101
                $classes = ClassInfo::subclassesFor(static::class);
102
103
                $concrete = array();
104
                foreach ($classes as $variantclass) {
105
                    $ref = new ReflectionClass($variantclass);
106
                    if ($ref->isInstantiable()) {
107
                        $variant = singleton($variantclass);
108
                        if ($variant->appliesToEnvironment()) {
109
                            $concrete[$variantclass] = $variant;
110
                        }
111
                    }
112
                }
113
114
                self::$variants = $concrete;
115
            }
116
117
            return self::$variants;
118
        } else {
119
            $key = $class . '!' . $includeSubclasses;
120
121
            if (!isset(self::$class_variants[$key])) {
122
                self::$class_variants[$key] = array();
123
124
                foreach (self::variants() as $variantclass => $instance) {
125
                    if ($instance->appliesTo($class, $includeSubclasses)) {
126
                        self::$class_variants[$key][$variantclass] = $instance;
127
                    }
128
                }
129
            }
130
131
            return self::$class_variants[$key];
132
        }
133
    }
134
135
    /**
136
     * Clear the cached variants
137
     */
138
    public static function clear_variant_cache()
139
    {
140
        self::$class_variants = [];
141
    }
142
143
    /** Holds a cache of SearchVariant_Caller instances, one for each class/includeSubclasses setting */
144
    protected static $call_instances = array();
145
146
    /**
147
     * Lets you call any function on all variants that support it, in the same manner as "Object#extend" calls
148
     * a method from extensions.
149
     *
150
     * Usage: SearchVariant::with(...)->call($method, $arg1, ...);
151
     *
152
     * @static
153
     *
154
     * @param string $class - (Optional) a classname. If passed, only variants that apply to that class will be checked / called
155
     *
156
     * @param bool $includeSubclasses - (Optional) If false, only variants that apply strictly to the passed class or its super-classes
157
     * will be checked. If true (the default), variants that apply to any sub-class of the passed class with also be checked
158
     *
159
     * @return SearchVariant_Caller An object with one method, call()
160
     */
161
    public static function with($class = null, $includeSubclasses = true)
162
    {
163
        // Make the cache key
164
        $key = $class ? $class . '!' . $includeSubclasses : '!';
165
        // If no SearchVariant_Caller instance yet, create it
166
        if (!isset(self::$call_instances[$key])) {
167
            self::$call_instances[$key] = new SearchVariant_Caller(self::variants($class, $includeSubclasses));
168
        }
169
        // Then return it
170
        return self::$call_instances[$key];
171
    }
172
173
    /**
174
     * Similar to {@link SearchVariant::with}, except will only use variants that apply to at least one of the classes
175
     * in the input array, where {@link SearchVariant::with} will run the query on the specific class you give it.
176
     *
177
     * @param string[] $classes
178
     * @return SearchVariant_Caller
179
     */
180
    public static function withCommon(array $classes = [])
181
    {
182
        // Allow caching
183
        $cacheKey = sha1(serialize($classes));
184
        if (isset(self::$call_instances[$cacheKey])) {
185
            return self::$call_instances[$cacheKey];
186
        }
187
188
        // Construct new array of variants applicable to at least one class in the list
189
        $commonVariants = [];
190
        foreach ($classes as $class => $options) {
191
            // BC for numerically indexed list of classes
192
            if (is_numeric($class) && !empty($options['class'])) {
193
                $class = $options['class']; // $options['class'] is assumed to exist throughout the code base
194
            }
195
196
            // Extract relevant class options
197
            $includeSubclasses = isset($options['include_children']) ? $options['include_children'] : true;
198
199
            // Get the variants for the current class
200
            $variantsForClass = self::variants($class, $includeSubclasses);
201
202
            // Merge the variants applicable to the current class into the list of common variants, using
203
            // the variant instance to replace any previous versions for the same class name (should be singleton
204
            // anyway).
205
            $commonVariants = array_replace($commonVariants, $variantsForClass);
206
        }
207
208
        // Cache for future calls
209
        self::$call_instances[$cacheKey] = new SearchVariant_Caller($commonVariants);
210
211
        return self::$call_instances[$cacheKey];
212
    }
213
214
    /**
215
     * A shortcut to with when calling without passing in a class,
216
     *
217
     * SearchVariant::call(...) ==== SearchVariant::with()->call(...);
218
     */
219
220
    public static function call($method, &...$args)
221
    {
222
        return self::with()->call($method, ...$args);
223
    }
224
225
    /**
226
     * Get the current state of every variant
227
     * @static
228
     * @return array
229
     */
230
    public static function current_state($class = null, $includeSubclasses = true)
231
    {
232
        $state = array();
233
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
234
            $state[$variant] = $instance->currentState();
235
        }
236
        return $state;
237
    }
238
239
    /**
240
     * Activate all the states in the passed argument
241
     * @static
242
     * @param array $state A set of (string)$variantClass => (any)$state pairs , e.g. as returned by
243
     * SearchVariant::current_state()
244
     * @return void
245
     */
246
    public static function activate_state($state)
247
    {
248
        foreach (self::variants() as $variant => $instance) {
249
            if (isset($state[$variant]) && $instance->appliesToEnvironment()) {
250
                $instance->activateState($state[$variant]);
251
            }
252
        }
253
    }
254
255
    /**
256
     * Return an iterator that, when used in a for loop, activates one combination of reindex states per loop, and restores
257
     * back to the original state at the end
258
     * @static
259
     * @param string $class - The class name to get variants for
260
     * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
261
     * @return SearchVariant_ReindexStateIteratorRet - The iterator to foreach loop over
0 ignored issues
show
The type SilverStripe\FullTextSea...ReindexStateIteratorRet was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
262
     */
263
    public static function reindex_states($class = null, $includeSubclasses = true)
264
    {
265
        $allstates = array();
266
267
        foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
268
            if ($states = $instance->reindexStates()) {
269
                $allstates[$variant] = $states;
270
            }
271
        }
272
273
        return $allstates ? new CombinationsArrayIterator($allstates) : array(array());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $allstates ? new ...tates) : array(array()) returns the type SilverStripe\FullTextSea...or|array<integer,array> which is incompatible with the documented return type SilverStripe\FullTextSea...ReindexStateIteratorRet.
Loading history...
274
    }
275
276
277
    /**
278
     * Add new filter field to index safely.
279
     *
280
     * This method will respect existing filters with the same field name that
281
     * correspond to multiple classes
282
     *
283
     * @param SearchIndex $index
284
     * @param string $name Field name
285
     * @param array $field Field spec
286
     */
287
    protected function addFilterField($index, $name, $field)
288
    {
289
        // If field already exists, make sure to merge origin / base fields
290
        if (isset($index->filterFields[$name])) {
291
            $field['base'] = $this->mergeClasses(
292
                $index->filterFields[$name]['base'],
293
                $field['base']
294
            );
295
            $field['origin'] = $this->mergeClasses(
296
                $index->filterFields[$name]['origin'],
297
                $field['origin']
298
            );
299
        }
300
301
        $index->filterFields[$name] = $field;
302
    }
303
304
    /**
305
     * Merge sets of (or individual) class names together for a search index field.
306
     *
307
     * If there is only one unique class name, then just return it as a string instead of array.
308
     *
309
     * @param array|string $left Left class(es)
310
     * @param array|string $right Right class(es)
311
     * @return array|string List of classes, or single class
312
     */
313
    protected function mergeClasses($left, $right)
314
    {
315
        // Merge together and remove dupes
316
        if (!is_array($left)) {
317
            $left = array($left);
318
        }
319
        if (!is_array($right)) {
320
            $right = array($right);
321
        }
322
        $merged = array_values(array_unique(array_merge($left, $right)));
323
324
        // If there is only one item, return it as a single string
325
        if (count($merged) === 1) {
326
            return reset($merged);
327
        }
328
        return $merged;
329
    }
330
}
331