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