Passed
Push — hans/valid-classes ( 374646...9e405c )
by Simon
05:57
created

SolrCoreService   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Test Coverage

Coverage 97.1%

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 131
c 19
b 0
f 0
dl 0
loc 462
ccs 134
cts 138
cp 0.971
rs 8.96
wmc 43

21 Methods

Rating   Name   Duplication   Size   Complexity  
A coreReload() 0 10 1
A coreCreate() 0 16 2
A checkReflection() 0 5 1
A updateItems() 0 24 5
A getValidIndexes() 0 12 4
A coreStatus() 0 9 1
A coreIsActive() 0 3 1
A coreUnload() 0 9 1
A filterIndexes() 0 13 5
A __construct() 0 8 1
A setInDebugMode() 0 5 1
A doManipulate() 0 11 2
A isInDebugMode() 0 3 1
A setClient() 0 5 1
A isValidClass() 0 5 1
A getFactory() 0 8 1
A getValidClasses() 0 12 3
A getUpdate() 0 25 5
A getClient() 0 3 1
A getSolrVersion() 0 20 3
A updateIndex() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like SolrCoreService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SolrCoreService, and based on these observations, apply Extract Interface, too.

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