Passed
Push — hans/Index-all-fluent-options ( 33af16...2afa88 )
by Simon
07:48
created

SolrCoreService::updateItems()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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