Passed
Push — hans/addAllFields ( 30dc29...48c3f4 )
by Simon
07:26 queued 01:24
created

SolrCoreService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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