Passed
Push — feature/Logging ( 0ade54 )
by Simon
04:18
created

SolrCoreService::coreStatus()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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