Passed
Push — hans/core-extraction ( 13ead0...ebda23 )
by Simon
06:01 queued 20s
created

SolrCoreService   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 99
dl 0
loc 322
rs 9.68
c 19
b 0
f 0
wmc 34

13 Methods

Rating   Name   Duplication   Size   Complexity  
A doManipulate() 0 11 2
A getClassesInHierarchy() 0 7 2
A isValidClass() 0 5 1
A getFactory() 0 8 1
A checkReflection() 0 5 1
A getValidClasses() 0 9 2
A getUpdate() 0 25 5
A updateItems() 0 24 5
A getValidIndexes() 0 12 4
A updateIndex() 0 7 2
A getSolrVersion() 0 20 3
A filterIndexes() 0 13 5
A __construct() 0 8 1
1
<?php
2
3
namespace Firesphere\SolrSearch\Services;
4
5
use Exception;
6
use Firesphere\SolrSearch\Factories\DocumentFactory;
7
use Firesphere\SolrSearch\Helpers\FieldResolver;
8
use Firesphere\SolrSearch\Indexes\BaseIndex;
9
use Firesphere\SolrSearch\Traits\CoreAdminTrait;
10
use Firesphere\SolrSearch\Traits\CoreServiceTrait;
11
use GuzzleHttp\Client as GuzzleClient;
12
use GuzzleHttp\HandlerStack;
13
use LogicException;
14
use ReflectionClass;
15
use ReflectionException;
16
use SilverStripe\Control\Director;
17
use SilverStripe\Core\ClassInfo;
18
use SilverStripe\Core\Config\Configurable;
19
use SilverStripe\Core\Injector\Injectable;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\ORM\ArrayList;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\SS_List;
24
use Solarium\Client;
25
use Solarium\Core\Client\Adapter\Guzzle;
26
use Solarium\QueryType\Update\Result;
27
28
/**
29
 * Class SolrCoreService provides the base connection to Solr.
30
 *
31
 * Default service to connect to Solr and handle all base requirements to support Solr.
32
 * Default constants are available to support any set up.
33
 *
34
 * @package Firesphere\SolrSearch\Services
35
 */
36
class SolrCoreService
37
{
38
    use Injectable;
39
    use Configurable;
40
    use CoreServiceTrait;
41
    use CoreAdminTrait;
42
    /**
43
     * Unique ID in Solr
44
     */
45
    const ID_FIELD = 'id';
46
    /**
47
     * SilverStripe ID of the object
48
     */
49
    const CLASS_ID_FIELD = 'ObjectID';
50
    /**
51
     * Name of the field that can be used for queries
52
     */
53
    const CLASSNAME = 'ClassName';
54
    /**
55
     * Solr update types
56
     */
57
    const DELETE_TYPE_ALL = 'deleteall';
58
    /**
59
     * string
60
     */
61
    const DELETE_TYPE = 'delete';
62
    /**
63
     * string
64
     */
65
    const UPDATE_TYPE = 'update';
66
    /**
67
     * string
68
     */
69
    const CREATE_TYPE = 'create';
70
71
    /**
72
     * @var array Base indexes that exist
73
     */
74
    protected $baseIndexes = [];
75
    /**
76
     * @var array Valid indexes out of the base indexes
77
     */
78
    protected $validIndexes = [];
79
80
    /**
81
     * SolrCoreService constructor.
82
     *
83
     * @throws ReflectionException
84
     */
85
    public function __construct()
86
    {
87
        $config = static::config()->get('config');
88
        $this->client = new Client($config);
89
        $this->client->setAdapter(new Guzzle());
90
        $this->admin = $this->client->createCoreAdmin();
91
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
92
        $this->filterIndexes();
93
    }
94
95
    /**
96
     * Filter enabled indexes down to valid indexes that can be instantiated
97
     * or are allowed from config
98
     *
99
     * @throws ReflectionException
100
     */
101
    protected function filterIndexes(): void
102
    {
103
        $enabledIndexes = static::config()->get('indexes');
104
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
105
        foreach ($this->baseIndexes as $subindex) {
106
            // If the config of indexes is set, and the requested index isn't in it, skip addition
107
            // Or, the index simply doesn't exist, also a valid option
108
            if (!in_array($subindex, $enabledIndexes, true) ||
109
                !$this->checkReflection($subindex)
110
            ) {
111
                continue;
112
            }
113
            $this->validIndexes[] = $subindex;
114
        }
115
    }
116
117
    /**
118
     * Check if the class is instantiable
119
     *
120
     * @param $subindex
121
     * @return bool
122
     * @throws ReflectionException
123
     */
124
    protected function checkReflection($subindex): bool
125
    {
126
        $reflectionClass = new ReflectionClass($subindex);
127
128
        return $reflectionClass->isInstantiable();
129
    }
130
131
    /**
132
     * Update items in the list to Solr
133
     *
134
     * @param SS_List|DataObject $items
135
     * @param string $type
136
     * @param null|string $index
137
     * @return bool|Result
138
     * @throws ReflectionException
139
     * @throws Exception
140
     */
141
    public function updateItems($items, $type, $index = null)
142
    {
143
        $indexes = $this->getValidIndexes($index);
144
145
        $result = false;
146
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
147
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
148
149
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
150
151
        foreach ($indexes as $indexString) {
152
            /** @var BaseIndex $index */
153
            $index = Injector::inst()->get($indexString);
154
            $classes = $index->getClasses();
155
            $inArray = array_intersect($classes, $hierarchy);
156
            // No point in sending a delete|update|create for something that's not in the index
157
            if (!count($inArray)) {
158
                continue;
159
            }
160
161
            $result = $this->doManipulate($items, $type, $index);
162
        }
163
164
        return $result;
165
    }
166
167
    /**
168
     * Get valid indexes for the project
169
     *
170
     * @param null|string $index
171
     * @return array
172
     */
173
    public function getValidIndexes($index = null): ?array
174
    {
175
        if ($index && !in_array($index, $this->validIndexes, true)) {
176
            throw new LogicException('Incorrect index ' . $index);
177
        }
178
179
        if ($index) {
180
            return [$index];
181
        }
182
183
        // return the array values, to reset the keys
184
        return array_values($this->validIndexes);
185
    }
186
187
    /**
188
     * Execute the manipulation of solr documents
189
     *
190
     * @param SS_List $items
191
     * @param $type
192
     * @param BaseIndex $index
193
     * @return Result
194
     * @throws Exception
195
     */
196
    public function doManipulate($items, $type, BaseIndex $index): Result
197
    {
198
        $client = $index->getClient();
199
200
        $update = $this->getUpdate($items, $type, $index, $client);
201
        // commit immediately when in dev mode
202
        if (Director::isDev()) {
203
            $update->addCommit();
204
        }
205
206
        return $client->update($update);
207
    }
208
209
    /**
210
     * get the update object ready
211
     *
212
     * @param SS_List $items
213
     * @param string $type
214
     * @param BaseIndex $index
215
     * @param \Solarium\Core\Client\Client $client
216
     * @return mixed
217
     * @throws Exception
218
     */
219
    protected function getUpdate($items, $type, BaseIndex $index, \Solarium\Core\Client\Client $client)
220
    {
221
        // get an update query instance
222
        $update = $client->createUpdate();
223
224
        switch ($type) {
225
            case static::DELETE_TYPE:
226
                // By pushing to a single array, we have less memory usage and no duplicates
227
                // This is faster, and more efficient, because we only do one DB query
228
                $delete = $items->map('ID', 'ClassName')->toArray();
229
                array_walk($delete, static function (&$item, $key) {
230
                    $item = sprintf('%s-%s', $item, $key);
231
                });
232
                $update->addDeleteByIds(array_values($delete));
233
                // Remove the deletion array from memory
234
                break;
235
            case static::DELETE_TYPE_ALL:
236
                $update->addDeleteQuery('*:*');
237
                break;
238
            case static::UPDATE_TYPE:
239
            case static::CREATE_TYPE:
240
                $this->updateIndex($index, $items, $update);
241
        }
242
243
        return $update;
244
    }
245
246
    /**
247
     * Create the documents and add to the update
248
     *
249
     * @param BaseIndex $index
250
     * @param SS_List $items
251
     * @param \Solarium\QueryType\Update\Query\Query $update
252
     * @throws Exception
253
     */
254
    public function updateIndex($index, $items, $update): void
255
    {
256
        $fields = $index->getFieldsForIndexing();
257
        $factory = $this->getFactory($items);
258
        $docs = $factory->buildItems($fields, $index, $update);
259
        if (count($docs)) {
260
            $update->addDocuments($docs);
261
        }
262
    }
263
264
    /**
265
     * Get the document factory prepared
266
     *
267
     * @param SS_List $items
268
     * @return DocumentFactory
269
     */
270
    protected function getFactory($items): DocumentFactory
271
    {
272
        $factory = Injector::inst()->get(DocumentFactory::class);
273
        $factory->setItems($items);
274
        $factory->setClass($items->first()->ClassName);
275
        $factory->setDebug($this->isInDebugMode());
276
277
        return $factory;
278
    }
279
280
    /**
281
     * Is the given class a valid class to index
282
     * Does not discriminate against the indexes. All indexes are worth the same
283
     *
284
     * @param string $class
285
     * @return bool
286
     * @throws ReflectionException
287
     */
288
    public function isValidClass($class): bool
289
    {
290
        $classes = $this->getValidClasses();
291
292
        return in_array($class, $classes, true);
293
    }
294
295
    /**
296
     * Get all classes from all indexes and return them.
297
     * Used to get all classes that are to be indexed on change
298
     * Note, only base classes are in this object. A publish recursive is required
299
     * when any change from a relation is published.
300
     *
301
     * @return array
302
     * @throws ReflectionException
303
     */
304
    public function getValidClasses()
305
    {
306
        $indexes = $this->getValidIndexes();
307
        $classes = [];
308
        foreach ($indexes as $index) {
309
            $classes = $this->getClassesInHierarchy($index, $classes);
310
        }
311
312
        return array_unique($classes);
313
    }
314
315
    /**
316
     * Get the classes in hierarchy to see if it's valid
317
     *
318
     * @param $index
319
     * @param array $classes
320
     * @return array
321
     * @throws ReflectionException
322
     */
323
    protected function getClassesInHierarchy($index, array $classes): array
324
    {
325
        $indexClasses = singleton($index)->getClasses();
326
        foreach ($indexClasses as $class) {
327
            $classes = array_merge($classes, FieldResolver::getHierarchy($class, true));
328
        }
329
        return $classes;
330
    }
331
332
    /**
333
     * Check the Solr version to use
334
     *
335
     * @param HandlerStack|null $handler Used for testing the solr version
336
     * @return int
337
     */
338
    public function getSolrVersion($handler = null): int
339
    {
340
        $config = self::config()->get('config');
341
        $firstEndpoint = array_shift($config['endpoint']);
342
        $clientConfig = [
343
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
344
        ];
345
346
        if ($handler) {
347
            $clientConfig['handler'] = $handler;
348
        }
349
350
        $client = new GuzzleClient($clientConfig);
351
352
        $result = $client->get('solr/admin/info/system?wt=json');
353
        $result = json_decode($result->getBody(), 1);
354
355
        $version = version_compare('5.0.0', $result['lucene']['solr-spec-version']);
356
357
        return ($version > 0) ? 4 : 5;
358
    }
359
}
360