Passed
Push — sheepy/introspection ( 17b395...901708 )
by Simon
08:33 queued 05:46
created

SolrCoreService::getUpdate()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

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