Passed
Push — feature/tests ( 559bb5...be8439 )
by Simon
01:58
created

SolrCoreService   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Test Coverage

Coverage 99.16%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
wmc 36
eloc 112
c 12
b 0
f 0
dl 0
loc 338
ccs 118
cts 119
cp 0.9916
rs 9.52

17 Methods

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