Passed
Push — hans/valid-classes ( 9e405c...d896ec )
by Simon
06:06
created

SolrCoreService::getClassesInHierarchy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
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
     * Execute the manipulation of solr documents
300
     *
301
     * @param SS_List $items
302
     * @param $type
303
     * @param BaseIndex $index
304
     * @return Result
305
     * @throws Exception
306
     */
307 76
    public function doManipulate($items, $type, BaseIndex $index): Result
308
    {
309 76
        $client = $index->getClient();
310 76
311 76
        $update = $this->getUpdate($items, $type, $index, $client);
312 76
        // commit immediately when in dev mode
313 76
        if (Director::isDev()) {
314 76
            $update->addCommit();
315
        }
316
317
        return $client->update($update);
318 76
    }
319
320
    /**
321
     * get the update object ready
322
     *
323
     * @param SS_List $items
324
     * @param string $type
325
     * @param BaseIndex $index
326
     * @param \Solarium\Core\Client\Client $client
327
     * @return mixed
328
     * @throws Exception
329 76
     */
330
    protected function getUpdate($items, $type, BaseIndex $index, \Solarium\Core\Client\Client $client)
331 76
    {
332
        // get an update query instance
333 76
        $update = $client->createUpdate();
334
335
        switch ($type) {
336
            case static::DELETE_TYPE:
337
                // By pushing to a single array, we have less memory usage and no duplicates
338
                // This is faster, and more efficient, because we only do one DB query
339
                $delete = $items->map('ID', 'ClassName')->toArray();
340
                array_walk($delete, static function (&$item, $key) {
341
                    $item = sprintf('%s-%s', $item, $key);
342
                });
343
                $update->addDeleteByIds(array_values($delete));
344
                // Remove the deletion array from memory
345 9
                break;
346
            case static::DELETE_TYPE_ALL:
347 9
                $update->addDeleteQuery('*:*');
348
                break;
349 9
            case static::UPDATE_TYPE:
350
            case static::CREATE_TYPE:
351 9
                $this->updateIndex($index, $items, $update);
352 9
        }
353
354
        return $update;
355 9
    }
356
357
    /**
358
     * Create the documents and add to the update
359
     *
360
     * @param BaseIndex $index
361
     * @param SS_List $items
362
     * @param \Solarium\QueryType\Update\Query\Query $update
363
     * @throws Exception
364
     */
365
    public function updateIndex($index, $items, $update): void
366
    {
367
        $fields = $index->getFieldsForIndexing();
368 9
        $factory = $this->getFactory($items);
369
        $docs = $factory->buildItems($fields, $index, $update);
370
        if (count($docs)) {
371 9
            $update->addDocuments($docs);
372
        }
373
    }
374 9
375
    /**
376
     * Get the document factory prepared
377 5
     *
378 5
     * @param SS_List $items
379 5
     * @return DocumentFactory
380 5
     */
381 5
    protected function getFactory($items): DocumentFactory
382
    {
383 5
        $factory = Injector::inst()->get(DocumentFactory::class);
384 8
        $factory->setItems($items);
385 2
        $factory->setClass($items->first()->ClassName);
386 2
        $factory->setDebug($this->isInDebugMode());
387 6
388 1
        return $factory;
389 6
    }
390
391
    /**
392 9
     * Check if we are in debug mode
393
     *
394
     * @return bool
395
     */
396
    public function isInDebugMode(): bool
397
    {
398
        return $this->inDebugMode;
399
    }
400
401
    /**
402
     * Set the debug mode
403 6
     *
404
     * @param bool $inDebugMode
405 6
     * @return SolrCoreService
406 6
     */
407 6
    public function setInDebugMode(bool $inDebugMode): SolrCoreService
408 6
    {
409 6
        $this->inDebugMode = $inDebugMode;
410
411 6
        return $this;
412
    }
413
414
    /**
415
     * Is the given class a valid class to index
416
     * Does not discriminate against the indexes. All indexes are worth the same
417
     *
418
     * @param string $class
419 6
     * @return bool
420
     * @throws ReflectionException
421 6
     */
422 6
    public function isValidClass($class): bool
423 6
    {
424 6
        $classes = $this->getValidClasses();
425
426 6
        return in_array($class, $classes, true);
427
    }
428
429
    /**
430
     * Get all classes from all indexes and return them.
431
     * Used to get all classes that are to be indexed on change
432
     * Note, only base classes are in this object. A publish recursive is required
433
     * when any change from a relation is published.
434 6
     *
435
     * @return array
436 6
     * @throws ReflectionException
437
     */
438
    public function getValidClasses()
439
    {
440
        $indexes = $this->getValidIndexes();
441
        $classes = [];
442
        foreach ($indexes as $index) {
443
            $classes = $this->getClassesInHierarchy($index, $classes);
444
        }
445 3
446
        return array_unique($classes);
447 3
    }
448
449 3
    /**
450
     * Get the classes in hierarchy to see if it's valid
451
     *
452
     * @param $index
453
     * @param array $classes
454
     * @return array
455
     * @throws ReflectionException
456
     */
457
    protected function getClassesInHierarchy($index, array $classes): array
458 31
    {
459
        $indexClasses = singleton($index)->getClasses();
460 31
        foreach ($indexClasses as $class) {
461 31
            $classes = array_merge($classes, FieldResolver::getHierarchy($class, true));
462
        }
463 31
        return $classes;
464
    }
465
466 31
    /**
467 1
     * Check the Solr version to use
468
     *
469
     * @param HandlerStack|null $handler Used for testing the solr version
470 31
     * @return int
471
     */
472 31
    public function getSolrVersion($handler = null): int
473 31
    {
474
        $config = self::config()->get('config');
475 31
        $firstEndpoint = array_shift($config['endpoint']);
476
        $clientConfig = [
477 31
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
478
        ];
479
480
        if ($handler) {
481
            $clientConfig['handler'] = $handler;
482
        }
483
484
        $client = new GuzzleClient($clientConfig);
485 1
486
        $result = $client->get('solr/admin/info/system?wt=json');
487 1
        $result = json_decode($result->getBody(), 1);
488
489
        $version = version_compare('5.0.0', $result['lucene']['solr-spec-version']);
490
491
        return ($version > 0) ? 4 : 5;
492
    }
493
494
    /**
495
     * Get the client
496 1
     *
497
     * @return Client
498 1
     */
499
    public function getClient(): Client
500 1
    {
501
        return $this->client;
502
    }
503
504
    /**
505
     * Set the client
506
     *
507
     * @param Client $client
508
     * @return SolrCoreService
509
     */
510
    public function setClient($client): SolrCoreService
511
    {
512
        $this->client = $client;
513
514
        return $this;
515
    }
516
}
517