Passed
Push — hans/valid-classes ( 46ad4b )
by Simon
06:29
created

SolrCoreService::getValidClasses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 10
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
    public function __construct()
105
    {
106
        $config = static::config()->get('config');
107
        $this->client = new Client($config);
108
        $this->client->setAdapter(new Guzzle());
109
        $this->admin = $this->client->createCoreAdmin();
110
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
111
        $this->filterIndexes();
112
    }
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
    protected function filterIndexes(): void
121
    {
122
        $enabledIndexes = static::config()->get('indexes');
123
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
124
        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
            if (!in_array($subindex, $enabledIndexes, true) ||
128
                !$this->checkReflection($subindex)
129
            ) {
130
                continue;
131
            }
132
            $this->validIndexes[] = $subindex;
133
        }
134
    }
135
136
    /**
137
     * Check if the class is instantiable
138
     *
139
     * @param $subindex
140
     * @return bool
141
     * @throws ReflectionException
142
     */
143
    protected function checkReflection($subindex): bool
144
    {
145
        $reflectionClass = new ReflectionClass($subindex);
146
147
        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
    public function coreCreate($core, $configStore): bool
160
    {
161
        $action = $this->admin->createCreate();
162
163
        $action->setCore($core);
164
        $action->setInstanceDir($configStore->instanceDir($core));
165
        $this->admin->setAction($action);
166
        try {
167
            $response = $this->client->coreAdmin($this->admin);
168
169
            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
    public function coreReload($core)
185
    {
186
        $reload = $this->admin->createReload();
187
        $reload->setCore($core);
188
189
        $this->admin->setAction($reload);
190
191
        $response = $this->client->coreAdmin($this->admin);
192
193
        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
    public function coreIsActive($core)
204
    {
205
        return $this->coreStatus($core);
206
    }
207
208
    /**
209
     * Get the core status
210
     *
211
     * @param string $core
212
     * @return StatusResult|null
213
     */
214
    public function coreStatus($core)
215
    {
216
        $status = $this->admin->createStatus();
217
        $status->setCore($core);
218
219
        $this->admin->setAction($status);
220
        $response = $this->client->coreAdmin($this->admin);
221
222
        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
    public function coreUnload($core)
232
    {
233
        $unload = $this->admin->createUnload();
234
        $unload->setCore($core);
235
236
        $this->admin->setAction($unload);
237
        $response = $this->client->coreAdmin($this->admin);
238
239
        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
    public function updateItems($items, $type, $index = null)
253
    {
254
        $indexes = $this->getValidIndexes($index);
255
256
        $result = false;
257
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
258
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
259
260
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
261
262
        foreach ($indexes as $indexString) {
263
            /** @var BaseIndex $index */
264
            $index = Injector::inst()->get($indexString);
265
            $classes = $index->getClasses();
266
            $inArray = array_intersect($classes, $hierarchy);
267
            // No point in sending a delete|update|create for something that's not in the index
268
            if (!count($inArray)) {
269
                continue;
270
            }
271
272
            $result = $this->doManipulate($items, $type, $index);
273
        }
274
275
        return $result;
276
    }
277
278
    /**
279
     * Get valid indexes for the project
280
     *
281
     * @param null|string $index
282
     * @return array
283
     */
284
    public function getValidIndexes($index = null): ?array
285
    {
286
        if ($index && !in_array($index, $this->validIndexes, true)) {
287
            throw new LogicException('Incorrect index ' . $index);
288
        }
289
290
        if ($index) {
291
            return [$index];
292
        }
293
294
        // return the array values, to reset the keys
295
        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
301
     * @return array
302
     */
303
    public function getValidClasses()
304
    {
305
        $indexes = $this->getValidIndexes();
306
        $classes = [];
307
        foreach ($indexes as $index) {
308
            $indexClasses = singleton($index)->getClasses();
309
            $classes = array_merge($classes, $indexClasses);
310
        }
311
312
        return array_unique($classes);
313
    }
314
315
    /**
316
     * Is the given class a valid class to index
317
     *
318
     * @param string $class
319
     * @return bool
320
     */
321
    public function isValidClass($class): bool
322
    {
323
        $classes = $this->getValidClasses();
324
325
        return in_array($class, $classes, true);
326
    }
327
328
    /**
329
     * Execute the manipulation of solr documents
330
     *
331
     * @param SS_List $items
332
     * @param $type
333
     * @param BaseIndex $index
334
     * @return Result
335
     * @throws Exception
336
     */
337
    public function doManipulate($items, $type, BaseIndex $index): Result
338
    {
339
        $client = $index->getClient();
340
341
        $update = $this->getUpdate($items, $type, $index, $client);
342
        // commit immediately when in dev mode
343
        if (Director::isDev()) {
344
            $update->addCommit();
345
        }
346
347
        return $client->update($update);
348
    }
349
350
    /**
351
     * get the update object ready
352
     *
353
     * @param SS_List $items
354
     * @param string $type
355
     * @param BaseIndex $index
356
     * @param \Solarium\Core\Client\Client $client
357
     * @return mixed
358
     * @throws Exception
359
     */
360
    protected function getUpdate($items, $type, BaseIndex $index, \Solarium\Core\Client\Client $client)
361
    {
362
        // get an update query instance
363
        $update = $client->createUpdate();
364
365
        switch ($type) {
366
            case static::DELETE_TYPE:
367
                // By pushing to a single array, we have less memory usage and no duplicates
368
                // This is faster, and more efficient, because we only do one DB query
369
                $delete = $items->map('ID', 'ClassName')->toArray();
370
                array_walk($delete, static function (&$item, $key) {
371
                    $item = sprintf('%s-%s', $item, $key);
372
                });
373
                $update->addDeleteByIds(array_values($delete));
374
                // Remove the deletion array from memory
375
                break;
376
            case static::DELETE_TYPE_ALL:
377
                $update->addDeleteQuery('*:*');
378
                break;
379
            case static::UPDATE_TYPE:
380
            case static::CREATE_TYPE:
381
                $this->updateIndex($index, $items, $update);
382
        }
383
384
        return $update;
385
    }
386
387
    /**
388
     * Create the documents and add to the update
389
     *
390
     * @param BaseIndex $index
391
     * @param SS_List $items
392
     * @param \Solarium\QueryType\Update\Query\Query $update
393
     * @throws Exception
394
     */
395
    public function updateIndex($index, $items, $update): void
396
    {
397
        $fields = $index->getFieldsForIndexing();
398
        $factory = $this->getFactory($items);
399
        $docs = $factory->buildItems($fields, $index, $update);
400
        if (count($docs)) {
401
            $update->addDocuments($docs);
402
        }
403
    }
404
405
    /**
406
     * Get the document factory prepared
407
     *
408
     * @param SS_List $items
409
     * @return DocumentFactory
410
     */
411
    protected function getFactory($items): DocumentFactory
412
    {
413
        $factory = Injector::inst()->get(DocumentFactory::class);
414
        $factory->setItems($items);
415
        $factory->setClass($items->first()->ClassName);
416
        $factory->setDebug($this->isInDebugMode());
417
418
        return $factory;
419
    }
420
421
    /**
422
     * Check if we are in debug mode
423
     *
424
     * @return bool
425
     */
426
    public function isInDebugMode(): bool
427
    {
428
        return $this->inDebugMode;
429
    }
430
431
    /**
432
     * Set the debug mode
433
     *
434
     * @param bool $inDebugMode
435
     * @return SolrCoreService
436
     */
437
    public function setInDebugMode(bool $inDebugMode): SolrCoreService
438
    {
439
        $this->inDebugMode = $inDebugMode;
440
441
        return $this;
442
    }
443
444
    /**
445
     * Check the Solr version to use
446
     *
447
     * @param HandlerStack|null $handler Used for testing the solr version
448
     * @return int
449
     */
450
    public function getSolrVersion($handler = null): int
451
    {
452
        $config = self::config()->get('config');
453
        $firstEndpoint = array_shift($config['endpoint']);
454
        $clientConfig = [
455
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
456
        ];
457
458
        if ($handler) {
459
            $clientConfig['handler'] = $handler;
460
        }
461
462
        $client = new GuzzleClient($clientConfig);
463
464
        $result = $client->get('solr/admin/info/system?wt=json');
465
        $result = json_decode($result->getBody(), 1);
466
467
        $version = version_compare('5.0.0', $result['lucene']['solr-spec-version']);
468
469
        return ($version > 0) ? 4 : 5;
470
    }
471
472
    /**
473
     * Get the client
474
     *
475
     * @return Client
476
     */
477
    public function getClient(): Client
478
    {
479
        return $this->client;
480
    }
481
482
    /**
483
     * Set the client
484
     *
485
     * @param Client $client
486
     * @return SolrCoreService
487
     */
488
    public function setClient($client): SolrCoreService
489
    {
490
        $this->client = $client;
491
492
        return $this;
493
    }
494
}
495