Passed
Push — hans/Index-all-fluent-options ( 42a725...2f1f10 )
by Simon
07:16
created

SolrCoreService   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Test Coverage

Coverage 96.85%

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 121
c 19
b 0
f 0
dl 0
loc 421
ccs 123
cts 127
cp 0.9685
rs 9.28
wmc 39

19 Methods

Rating   Name   Duplication   Size   Complexity  
A coreReload() 0 10 1
A setInDebugMode() 0 5 1
A doManipulate() 0 11 2
A isInDebugMode() 0 3 1
A setClient() 0 5 1
A coreCreate() 0 16 2
A getFactory() 0 8 1
A checkReflection() 0 5 1
A getUpdate() 0 25 5
A updateItems() 0 24 5
A getValidIndexes() 0 12 4
A coreStatus() 0 9 1
A getClient() 0 3 1
A coreIsActive() 0 3 1
A coreUnload() 0 9 1
A getSolrVersion() 0 20 3
A updateIndex() 0 7 2
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\Helpers\SolrLogger;
9
use Firesphere\SolrSearch\Indexes\BaseIndex;
10
use Firesphere\SolrSearch\Interfaces\ConfigStore;
11
use GuzzleHttp\Client as GuzzleClient;
12
use GuzzleHttp\Exception\GuzzleException;
13
use GuzzleHttp\HandlerStack;
14
use LogicException;
15
use ReflectionClass;
16
use ReflectionException;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Core\ClassInfo;
19
use SilverStripe\Core\Config\Configurable;
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\Server\CoreAdmin\Query\Query;
27
use Solarium\QueryType\Server\CoreAdmin\Result\StatusResult;
28
use Solarium\QueryType\Update\Result;
29
30
/**
31
 * Class SolrCoreService provides the base connection to Solr.
32
 *
33
 * Default service to connect to Solr and handle all base requirements to support Solr.
34
 * Default constants are available to support any set up.
35
 * @package Firesphere\SolrSearch\Services
36
 */
37
class SolrCoreService
38
{
39
    /**
40
     * Unique ID in Solr
41
     */
42
    const ID_FIELD = 'id';
43
    /**
44
     * SilverStripe ID of the object
45
     */
46
    const CLASS_ID_FIELD = 'ObjectID';
47
    /**
48
     * Name of the field that can be used for queries
49
     */
50
    const CLASSNAME = 'ClassName';
51
    /**
52
     * Solr update types
53
     */
54
    const DELETE_TYPE_ALL = 'deleteall';
55
    /**
56
     * string
57
     */
58
    const DELETE_TYPE = 'delete';
59
    /**
60
     * string
61
     */
62
    const UPDATE_TYPE = 'update';
63
    /**
64
     * string
65
     */
66
    const CREATE_TYPE = 'create';
67
68
69
    use Configurable;
70
71
    /**
72
     * @var Client The current client
73
     */
74
    protected $client;
75
    /**
76
     * @var array Base indexes that exist
77
     */
78
    protected $baseIndexes = [];
79
    /**
80
     * @var array Valid indexes out of the base indexes
81
     */
82
    protected $validIndexes = [];
83
    /**
84
     * @var Query A core admin object
85
     */
86
    protected $admin;
87
88
    /**
89
     * Add debugging information
90
     *
91
     * @var bool
92
     */
93
    protected $inDebugMode = false;
94
95
96
    /**
97
     * SolrCoreService constructor.
98
     *
99
     * @throws ReflectionException
100
     */
101 73
    public function __construct()
102
    {
103 73
        $config = static::config()->get('config');
104 73
        $this->client = new Client($config);
105 73
        $this->client->setAdapter(new Guzzle());
106 73
        $this->admin = $this->client->createCoreAdmin();
107 73
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
108 73
        $this->filterIndexes();
109 73
    }
110
111
    /**
112
     * Filter enabled indexes down to valid indexes that can be instantiated
113
     * or are allowed from config
114
     *
115
     * @throws ReflectionException
116
     */
117 73
    protected function filterIndexes(): void
118
    {
119 73
        $enabledIndexes = static::config()->get('indexes');
120 73
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
121 73
        foreach ($this->baseIndexes as $subindex) {
122
            // If the config of indexes is set, and the requested index isn't in it, skip addition
123
            // Or, the index simply doesn't exist, also a valid option
124 73
            if (!in_array($subindex, $enabledIndexes, true) ||
125 73
                !$this->checkReflection($subindex)
126
            ) {
127 73
                continue;
128
            }
129 73
            $this->validIndexes[] = $subindex;
130
        }
131 73
    }
132
133
    /**
134
     * Check if the class is instantiable
135
     *
136
     * @param $subindex
137
     * @return bool
138
     * @throws ReflectionException
139
     */
140 73
    protected function checkReflection($subindex): bool
141
    {
142 73
        $reflectionClass = new ReflectionClass($subindex);
143
144 73
        return $reflectionClass->isInstantiable();
145
    }
146
147
    /**
148
     * Create a new core
149
     *
150
     * @param $core string - The name of the core
151
     * @param ConfigStore $configStore
152
     * @return bool
153
     * @throws Exception
154
     * @throws GuzzleException
155
     */
156 2
    public function coreCreate($core, $configStore): bool
157
    {
158 2
        $action = $this->admin->createCreate();
159
160 2
        $action->setCore($core);
161 2
        $action->setInstanceDir($configStore->instanceDir($core));
162 2
        $this->admin->setAction($action);
163
        try {
164 2
            $response = $this->client->coreAdmin($this->admin);
165
166 2
            return $response->getWasSuccessful();
167
        } catch (Exception $e) {
168
            $solrLogger = new SolrLogger();
169
            $solrLogger->saveSolrLog('Config');
170
171
            throw new Exception($e);
172
        }
173
    }
174
175
    /**
176
     * Reload the given core
177
     * @param $core
178
     * @return StatusResult|null
179
     */
180 30
    public function coreReload($core): ?StatusResult
181
    {
182 30
        $reload = $this->admin->createReload();
183 30
        $reload->setCore($core);
184
185 30
        $this->admin->setAction($reload);
186
187 30
        $response = $this->client->coreAdmin($this->admin);
188
189 30
        return $response->getStatusResult();
190
    }
191
192
    /**
193
     * Check the status of a core
194
     * @param string $core
195
     * @return StatusResult|null
196
     * @deprecated backward compatibility stub
197
     */
198 1
    public function coreIsActive($core): ?StatusResult
199
    {
200 1
        return $this->coreStatus($core);
201
    }
202
203
    /**
204
     * Get the core status
205
     *
206
     * @param string $core
207
     * @return StatusResult|null
208
     */
209 30
    public function coreStatus($core): ?StatusResult
210
    {
211 30
        $status = $this->admin->createStatus();
212 30
        $status->setCore($core);
213
214 30
        $this->admin->setAction($status);
215 30
        $response = $this->client->coreAdmin($this->admin);
216
217 30
        return $response->getStatusResult();
218
    }
219
220
    /**
221
     * Remove a core from Solr
222
     *
223
     * @param string $core core name
224
     * @return StatusResult|null A result is successful
225
     */
226 1
    public function coreUnload($core): ?StatusResult
227
    {
228 1
        $unload = $this->admin->createUnload();
229 1
        $unload->setCore($core);
230
231 1
        $this->admin->setAction($unload);
232 1
        $response = $this->client->coreAdmin($this->admin);
233
234 1
        return $response->getStatusResult();
235
    }
236
237
    /**
238
     * Update items in the list to Solr
239
     *
240
     * @param SS_List|DataObject $items
241
     * @param string $type
242
     * @param null|string $index
243
     * @return bool|Result
244
     * @throws ReflectionException
245
     * @throws Exception
246
     */
247 72
    public function updateItems($items, $type, $index = null)
248
    {
249 72
        $indexes = $this->getValidIndexes($index);
250
251 72
        $result = false;
252 72
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
253 72
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
254
255 72
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
256
257 72
        foreach ($indexes as $indexString) {
258
            /** @var BaseIndex $index */
259 72
            $index = Injector::inst()->get($indexString);
260 72
            $classes = $index->getClasses();
261 72
            $inArray = array_intersect($classes, $hierarchy);
262
            // No point in sending a delete|update|create for something that's not in the index
263 72
            if (!count($inArray)) {
264 72
                continue;
265
            }
266
267 7
            $result = $this->doManipulate($items, $type, $index);
268
        }
269
270 72
        return $result;
271
    }
272
273
    /**
274
     * Get valid indexes for the project
275
     *
276
     * @param null|string $index
277
     * @return array
278
     */
279 73
    public function getValidIndexes($index = null): ?array
280
    {
281 73
        if ($index && !in_array($index, $this->validIndexes, true)) {
282 1
            throw new LogicException('Incorrect index ' . $index);
283
        }
284
285 73
        if ($index) {
286 5
            return [$index];
287
        }
288
289
        // return the array values, to reset the keys
290 73
        return array_values($this->validIndexes);
291
    }
292
293
    /**
294
     * Execute the manipulation of solr documents
295
     *
296
     * @param SS_List $items
297
     * @param $type
298
     * @param BaseIndex $index
299
     * @return Result
300
     * @throws Exception
301
     */
302 9
    public function doManipulate($items, $type, BaseIndex $index): Result
303
    {
304 9
        $client = $index->getClient();
305
306 9
        $update = $this->getUpdate($items, $type, $index, $client);
307
        // commit immediately when in dev mode
308 9
        if (Director::isDev()) {
309 9
            $update->addCommit();
310
        }
311
312 9
        return $client->update($update);
313
    }
314
315
    /**
316
     * get the update object ready
317
     *
318
     * @param SS_List $items
319
     * @param string $type
320
     * @param BaseIndex $index
321
     * @param \Solarium\Core\Client\Client $client
322
     * @return mixed
323
     * @throws Exception
324
     */
325 9
    protected function getUpdate($items, $type, BaseIndex $index, \Solarium\Core\Client\Client $client)
326
    {
327
        // get an update query instance
328 9
        $update = $client->createUpdate();
329
330
        switch ($type) {
331 9
            case static::DELETE_TYPE:
332
                // By pushing to a single array, we have less memory usage and no duplicates
333
                // This is faster, and more efficient, because we only do one DB query
334 5
                $delete = $items->map('ID', 'ClassName')->toArray();
335 5
                array_walk($delete, static function (&$item, $key) {
336 5
                    $item = sprintf('%s-%s', $item, $key);
337 5
                });
338 5
                $update->addDeleteByIds(array_values($delete));
339
                // Remove the deletion array from memory
340 5
                break;
341 8
            case static::DELETE_TYPE_ALL:
342 2
                $update->addDeleteQuery('*:*');
343 2
                break;
344 6
            case static::UPDATE_TYPE:
345 1
            case static::CREATE_TYPE:
346 6
                $this->updateIndex($index, $items, $update);
347
        }
348
349 9
        return $update;
350
    }
351
352
    /**
353
     * Create the documents and add to the update
354
     *
355
     * @param BaseIndex $index
356
     * @param SS_List $items
357
     * @param \Solarium\QueryType\Update\Query\Query $update
358
     * @throws Exception
359
     */
360 6
    public function updateIndex($index, $items, $update): void
361
    {
362 6
        $fields = $index->getFieldsForIndexing();
363 6
        $factory = $this->getFactory($items);
364 6
        $docs = $factory->buildItems($fields, $index, $update);
365 6
        if (count($docs)) {
366 6
            $update->addDocuments($docs);
367
        }
368 6
    }
369
370
    /**
371
     * Get the document factory prepared
372
     *
373
     * @param SS_List $items
374
     * @return DocumentFactory
375
     */
376 6
    protected function getFactory($items): DocumentFactory
377
    {
378 6
        $factory = Injector::inst()->get(DocumentFactory::class);
379 6
        $factory->setItems($items);
380 6
        $factory->setClass($items->first()->ClassName);
381 6
        $factory->setDebug($this->isInDebugMode());
382
383 6
        return $factory;
384
    }
385
386
    /**
387
     * Check if we are in debug mode
388
     *
389
     * @return bool
390
     */
391 6
    public function isInDebugMode(): bool
392
    {
393 6
        return $this->inDebugMode;
394
    }
395
396
    /**
397
     * Set the debug mode
398
     *
399
     * @param bool $inDebugMode
400
     * @return SolrCoreService
401
     */
402 70
    public function setInDebugMode(bool $inDebugMode): SolrCoreService
403
    {
404 70
        $this->inDebugMode = $inDebugMode;
405
406 70
        return $this;
407
    }
408
409
    /**
410
     * Check the Solr version to use
411
     *
412
     * @param HandlerStack|null $handler Used for testing the solr version
413
     * @return int
414
     */
415 30
    public function getSolrVersion($handler = null): int
416
    {
417 30
        $config = self::config()->get('config');
418 30
        $firstEndpoint = array_shift($config['endpoint']);
419
        $clientConfig = [
420 30
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
421
        ];
422
423 30
        if ($handler) {
424 1
            $clientConfig['handler'] = $handler;
425
        }
426
427 30
        $client = new GuzzleClient($clientConfig);
428
429 30
        $result = $client->get('solr/admin/info/system?wt=json');
430 30
        $result = json_decode($result->getBody(), 1);
431
432 30
        $version = version_compare('5.0.0', $result['lucene']['solr-spec-version']);
433
434 30
        return ($version > 0) ? 4 : 5;
435
    }
436
437
    /**
438
     * Get the client
439
     *
440
     * @return Client
441
     */
442 1
    public function getClient(): Client
443
    {
444 1
        return $this->client;
445
    }
446
447
    /**
448
     * Set the client
449
     *
450
     * @param Client $client
451
     * @return SolrCoreService
452
     */
453 1
    public function setClient($client): SolrCoreService
454
    {
455 1
        $this->client = $client;
456
457 1
        return $this;
458
    }
459
}
460