Passed
Push — hans/valid-classes ( 1110df...3598c3 )
by Simon
11:18
created

SolrCoreService::coreCreate()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.1922

Importance

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