SolrCoreService::updateIndex()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
rs 10
ccs 5
cts 5
cp 1
cc 2
nc 2
nop 3
crap 2
1
<?php
2
/**
3
 * class SolrCoreService|Firesphere\SolrSearch\Services\SolrCoreService Base service for communicating with the core
4
 *
5
 * @package Firesphere\Solr\Search
6
 * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo
7
 * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy
8
 */
9
10
namespace Firesphere\SolrSearch\Services;
11
12
use Exception;
13
use Firesphere\SolrSearch\Factories\DocumentFactory;
14
use Firesphere\SolrSearch\Helpers\FieldResolver;
15
use Firesphere\SolrSearch\Indexes\BaseIndex;
16
use Firesphere\SolrSearch\Traits\CoreAdminTrait;
17
use Firesphere\SolrSearch\Traits\CoreServiceTrait;
18
use GuzzleHttp\Client as GuzzleClient;
19
use GuzzleHttp\HandlerStack;
20
use Http\Discovery\HttpClientDiscovery;
21
use Http\Discovery\Psr17FactoryDiscovery;
22
use LogicException;
23
use ReflectionClass;
24
use ReflectionException;
25
use SilverStripe\Core\ClassInfo;
26
use SilverStripe\Core\Config\Configurable;
27
use SilverStripe\Core\Injector\Injectable;
28
use SilverStripe\Core\Injector\Injector;
29
use SilverStripe\ORM\ArrayList;
30
use SilverStripe\ORM\DataObject;
31
use SilverStripe\ORM\SS_List;
32
use Solarium\Client;
33
use Solarium\Core\Client\Adapter\Psr18Adapter;
34
use Solarium\Core\Client\Client as CoreClient;
35
use Solarium\QueryType\Update\Query\Query;
36
use Solarium\QueryType\Update\Result;
37
use Symfony\Component\EventDispatcher\EventDispatcher;
38
39
/**
40
 * Class SolrCoreService provides the base connection to Solr.
41
 *
42
 * Default service to connect to Solr and handle all base requirements to support Solr.
43
 * Default constants are available to support any set up.
44
 *
45
 * @package Firesphere\Solr\Search
46
 */
47
class SolrCoreService
48
{
49
    use Injectable;
50
    use Configurable;
51
    use CoreServiceTrait;
52
    use CoreAdminTrait;
53
54
    /**
55
     * Unique ID in Solr
56
     */
57
    const ID_FIELD = 'id';
58
    /**
59
     * SilverStripe ID of the object
60
     */
61
    const CLASS_ID_FIELD = 'ObjectID';
62
    /**
63
     * Name of the field that can be used for queries
64
     */
65
    const CLASSNAME = 'ClassName';
66
    /**
67
     * Solr update types
68
     */
69
    const DELETE_TYPE_ALL = 'deleteall';
70
    /**
71
     * string
72
     */
73
    const DELETE_TYPE = 'delete';
74
    /**
75
     * string
76
     */
77
    const UPDATE_TYPE = 'update';
78
    /**
79
     * string
80
     */
81
    const CREATE_TYPE = 'create';
82
83
    /**
84
     * @var array Base indexes that exist
85
     */
86
    protected $baseIndexes = [];
87
    /**
88
     * @var array Valid indexes out of the base indexes
89
     */
90
    protected $validIndexes = [];
91
    /**
92
     * @var array Available config versions
93
     */
94 88
    protected static $solr_versions = [
95
        "9.0.0",
96 88
        "7.0.0",
97 88
        "5.0.0",
98 88
        "4.0.0",
99 88
    ];
100 88
101 88
    /**
102 88
     * SolrCoreService constructor.
103
     *
104
     * @throws ReflectionException
105
     */
106
    public function __construct()
107
    {
108
        $config = static::config()->get('config');
109
        $httpClient = HTTPClientDiscovery::find();
110 88
        $requestFactory = Psr17FactoryDiscovery::findRequestFactory();
111
        $streamFactory = Psr17FactoryDiscovery::findStreamFactory();
112 88
        $eventDispatcher = new EventDispatcher();
113 88
        $adapter = new Psr18Adapter($httpClient, $requestFactory, $streamFactory);
114 88
        $this->client = new Client($adapter, $eventDispatcher, $config);
115
        $this->admin = $this->client->createCoreAdmin();
116
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
117 88
        $this->filterIndexes();
118 88
    }
119
120 88
    /**
121
     * Filter enabled indexes down to valid indexes that can be instantiated
122 88
     * or are allowed from config
123
     *
124 88
     * @throws ReflectionException
125
     */
126
    protected function filterIndexes(): void
127
    {
128
        $enabledIndexes = static::config()->get('indexes');
129
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
130
        foreach ($this->baseIndexes as $subindex) {
131
            // If the config of indexes is set, and the requested index isn't in it, skip addition
132
            // Or, the index simply doesn't exist, also a valid option
133 88
            if (!in_array($subindex, $enabledIndexes, true) ||
134
                !$this->checkReflection($subindex)
135 88
            ) {
136
                continue;
137 88
            }
138
            $this->validIndexes[] = $subindex;
139
        }
140
    }
141
142
    /**
143
     * Check if the class is instantiable
144
     *
145
     * @param $subindex
146
     * @return bool
147
     * @throws ReflectionException
148
     */
149
    protected function checkReflection($subindex): bool
150 9
    {
151
        $reflectionClass = new ReflectionClass($subindex);
152 9
153
        return $reflectionClass->isInstantiable();
154 8
    }
155 8
156 8
    /**
157
     * Update items in the list to Solr
158 8
     *
159
     * @param SS_List|DataObject $items
160 8
     * @param string $type
161
     * @param null|string $index
162 8
     * @return bool|Result
163 8
     * @throws ReflectionException
164 8
     * @throws Exception
165
     */
166 8
    public function updateItems($items, $type, $index = null)
167 7
    {
168
        $indexes = $this->getValidIndexes($index);
169
170 8
        $result = false;
171
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
172
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
173 8
174
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
175
176
        foreach ($indexes as $indexString) {
177
            /** @var BaseIndex $index */
178
            $index = Injector::inst()->get($indexString);
179
            $classes = $index->getClasses();
180
            $inArray = array_intersect($classes, $hierarchy);
181
            // No point in sending a delete|update|create for something that's not in the index
182 42
            if (!count($inArray)) {
183
                continue;
184 42
            }
185 1
186
            $result = $this->doManipulate($items, $type, $index);
187
        }
188 42
189 5
        return $result;
190
    }
191
192
    /**
193 41
     * Get valid indexes for the project
194
     *
195
     * @param null|string $index
196
     * @return array
197
     */
198
    public function getValidIndexes($index = null): array
199
    {
200
        if ($index && !in_array($index, $this->validIndexes, true)) {
201
            throw new LogicException('Incorrect index ' . $index);
202
        }
203
204
        if ($index) {
205 10
            return [$index];
206
        }
207 10
208
        // return the array values, to reset the keys
209 10
        return array_values($this->validIndexes);
210
    }
211
212
    /**
213 10
     * Execute the manipulation of solr documents
214
     *
215
     * @param SS_List $items
216
     * @param $type
217
     * @param BaseIndex $index
218
     * @return Result
219
     * @throws Exception
220
     */
221
    public function doManipulate($items, $type, BaseIndex $index): Result
222
    {
223
        $client = $index->getClient();
224
225
        $update = $this->getUpdate($items, $type, $index, $client);
226 10
227
        // commit immediately when in dev mode
228
229 10
        return $client->update($update);
230
    }
231
232 10
    /**
233
     * get the update object ready
234
     *
235 6
     * @param SS_List $items
236 6
     * @param string $type
237 6
     * @param BaseIndex $index
238 6
     * @param CoreClient $client
239 6
     * @return mixed
240
     * @throws Exception
241 6
     */
242 9
    protected function getUpdate($items, $type, BaseIndex $index, CoreClient $client)
243 2
    {
244 2
        // get an update query instance
245 7
        $update = $client->createUpdate();
246 1
247 7
        switch ($type) {
248 7
            case static::DELETE_TYPE:
249
                // By pushing to a single array, we have less memory usage and no duplicates
250 10
                // This is faster, and more efficient, because we only do one DB query
251
                $delete = $items->map('ID', 'ClassName')->toArray();
252 10
                array_walk($delete, static function (&$item, $key) {
253
                    $item = sprintf('%s-%s', $item, $key);
254
                });
255
                $update->addDeleteByIds(array_values($delete));
256
                // Remove the deletion array from memory
257
                break;
258
            case static::DELETE_TYPE_ALL:
259
                $update->addDeleteQuery('*:*');
260
                break;
261
            case static::UPDATE_TYPE:
262
            case static::CREATE_TYPE:
263 7
                $this->updateIndex($index, $items, $update);
264
                break;
265 7
        }
266 7
        $update->addCommit();
267 7
268 7
        return $update;
269 7
    }
270
271 7
    /**
272
     * Create the documents and add to the update
273
     *
274
     * @param BaseIndex $index
275
     * @param SS_List $items
276
     * @param Query $update
277
     * @throws Exception
278
     */
279 7
    public function updateIndex($index, $items, $update): void
280
    {
281 7
        $fields = $index->getFieldsForIndexing();
282 7
        $factory = $this->getFactory($items);
283 7
        $docs = $factory->buildItems($fields, $index, $update);
284 7
        if (count($docs)) {
285
            $update->addDocuments($docs);
286 7
        }
287
    }
288
289
    /**
290
     * Get the document factory prepared
291
     *
292
     * @param SS_List $items
293
     * @return DocumentFactory
294
     */
295 37
    protected function getFactory($items): DocumentFactory
296
    {
297 37
        $factory = Injector::inst()->get(DocumentFactory::class);
298 37
        $factory->setItems($items);
299
        $factory->setClass($items->first()->ClassName);
300 37
        $factory->setDebug($this->isDebug());
301
302
        return $factory;
303 37
    }
304 1
305
    /**
306
     * Check the Solr version to use
307 37
     * In version compare, we have the following results:
308
     *       1 means "result version is higher"
309 37
     *       0 means "result version is equal"
310
     *      -1 means "result version is lower"
311 37
     * We want to use the version "higher or equal to", because the
312 37
     * configs are for version X-and-up.
313
     * We loop through the versions available from high to low
314 37
     * therefore, if the version is lower, we want to check the next config version
315
     *
316 37
     * If no valid version is found, throw an error
317 1
     *
318
     * @param HandlerStack|null $handler Used for testing the solr version
319
     * @throws LogicException
320 37
     * @return int
321 1
     */
322
    public function getSolrVersion($handler = null): int
323
    {
324 37
        $config = self::config()->get('config');
325
        $firstEndpoint = array_shift($config['endpoint']);
326
        $clientConfig = [
327
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
328
        ];
329
330
        if ($handler) {
331
            $clientConfig['handler'] = $handler;
332
        }
333
334 37
        $clientOptions = $this->getSolrAuthentication($firstEndpoint);
335
336 37
        $client = new GuzzleClient($clientConfig);
337
338 37
        $result = $client->get('solr/admin/info/system?wt=json', $clientOptions);
339
        $result = json_decode($result->getBody(), 1);
340
341 37
        foreach (static::$solr_versions as $version) {
342 37
            $compare = version_compare($version, $result['lucene']['solr-spec-version']);
343
            if ($compare !== -1) {
344
                list($v) = explode('.', $version);
345
                return (int)$v;
346
            }
347 37
        }
348
349
        throw new LogicException('No valid version of Solr found!', 255);
350
    }
351
352
    /**
353
     * This will add the authentication headers to the request.
354
     * It's intended to become a helper in the end.
355
     *
356
     * @param array $firstEndpoint
357
     * @return array|array[]
358
     */
359
    private function getSolrAuthentication($firstEndpoint): array
360
    {
361
        $clientOptions = [];
362
363
        if (isset($firstEndpoint['username']) && isset($firstEndpoint['password'])) {
364
            $clientOptions = [
365
                'auth' => [
366
                    $firstEndpoint['username'],
367
                    $firstEndpoint['password']
368
                ]
369
            ];
370
        }
371
372
        return $clientOptions;
373
    }
374
}
375