Passed
Push — hans/dependency-resolving ( 99fd7b...5a9eb8 )
by Simon
24:15
created

SolrCoreService::getSolrAuthentication()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
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\Core\Client\Request;
36
use Solarium\QueryType\Update\Query\Query;
37
use Solarium\QueryType\Update\Result;
38
use Symfony\Component\EventDispatcher\EventDispatcher;
39
40
/**
41
 * Class SolrCoreService provides the base connection to Solr.
42
 *
43
 * Default service to connect to Solr and handle all base requirements to support Solr.
44
 * Default constants are available to support any set up.
45
 *
46
 * @package Firesphere\Solr\Search
47
 */
48
class SolrCoreService
49
{
50
    use Injectable;
51
    use Configurable;
52
    use CoreServiceTrait;
53
    use CoreAdminTrait;
54
55
    /**
56
     * Unique ID in Solr
57
     */
58
    const ID_FIELD = 'id';
59
    /**
60
     * SilverStripe ID of the object
61
     */
62
    const CLASS_ID_FIELD = 'ObjectID';
63
    /**
64
     * Name of the field that can be used for queries
65
     */
66
    const CLASSNAME = 'ClassName';
67
    /**
68
     * Solr update types
69
     */
70
    const DELETE_TYPE_ALL = 'deleteall';
71
    /**
72
     * string
73
     */
74
    const DELETE_TYPE = 'delete';
75
    /**
76
     * string
77
     */
78
    const UPDATE_TYPE = 'update';
79
    /**
80
     * string
81
     */
82
    const CREATE_TYPE = 'create';
83
84
    /**
85
     * @var array Base indexes that exist
86
     */
87
    protected $baseIndexes = [];
88
    /**
89
     * @var array Valid indexes out of the base indexes
90
     */
91
    protected $validIndexes = [];
92
    /**
93
     * @var array Available config versions
94
     */
95
    protected static $solr_versions = [
96
        "9.0.0",
97
        "7.0.0",
98
        "5.0.0",
99
        "4.0.0",
100
    ];
101
102
    /**
103
     * SolrCoreService constructor.
104
     *
105
     * @throws ReflectionException
106
     */
107
    public function __construct()
108
    {
109
        $config = static::config()->get('config');
110
        $httpClient = HTTPClientDiscovery::find();
111
        $requestFactory = Psr17FactoryDiscovery::findRequestFactory();
112
        $streamFactory = Psr17FactoryDiscovery::findStreamFactory();
113
        $eventDispatcher = new EventDispatcher();
114
        $adapter = new Psr18Adapter($httpClient, $requestFactory, $streamFactory);
115
        $this->client = new Client($adapter, $eventDispatcher, $config);
116
        $this->admin = $this->client->createCoreAdmin();
117
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
118
        $this->filterIndexes();
119
    }
120
121
    /**
122
     * Filter enabled indexes down to valid indexes that can be instantiated
123
     * or are allowed from config
124
     *
125
     * @throws ReflectionException
126
     */
127
    protected function filterIndexes(): void
128
    {
129
        $enabledIndexes = static::config()->get('indexes');
130
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
131
        foreach ($this->baseIndexes as $subindex) {
132
            // If the config of indexes is set, and the requested index isn't in it, skip addition
133
            // Or, the index simply doesn't exist, also a valid option
134
            if (!in_array($subindex, $enabledIndexes, true) ||
135
                !$this->checkReflection($subindex)
136
            ) {
137
                continue;
138
            }
139
            $this->validIndexes[] = $subindex;
140
        }
141
    }
142
143
    /**
144
     * Check if the class is instantiable
145
     *
146
     * @param $subindex
147
     * @return bool
148
     * @throws ReflectionException
149
     */
150
    protected function checkReflection($subindex): bool
151
    {
152
        $reflectionClass = new ReflectionClass($subindex);
153
154
        return $reflectionClass->isInstantiable();
155
    }
156
157
    /**
158
     * Update items in the list to Solr
159
     *
160
     * @param SS_List|DataObject $items
161
     * @param string $type
162
     * @param null|string $index
163
     * @return bool|Result
164
     * @throws ReflectionException
165
     * @throws Exception
166
     */
167
    public function updateItems($items, $type, $index = null)
168
    {
169
        $indexes = $this->getValidIndexes($index);
170
171
        $result = false;
172
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
173
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
174
175
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
176
177
        foreach ($indexes as $indexString) {
178
            /** @var BaseIndex $index */
179
            $index = Injector::inst()->get($indexString);
180
            $classes = $index->getClasses();
181
            $inArray = array_intersect($classes, $hierarchy);
182
            // No point in sending a delete|update|create for something that's not in the index
183
            if (!count($inArray)) {
184
                continue;
185
            }
186
187
            $result = $this->doManipulate($items, $type, $index);
188
        }
189
190
        return $result;
191
    }
192
193
    /**
194
     * Get valid indexes for the project
195
     *
196
     * @param null|string $index
197
     * @return array
198
     */
199
    public function getValidIndexes($index = null): array
200
    {
201
        if ($index && !in_array($index, $this->validIndexes, true)) {
202
            throw new LogicException('Incorrect index ' . $index);
203
        }
204
205
        if ($index) {
206
            return [$index];
207
        }
208
209
        // return the array values, to reset the keys
210
        return array_values($this->validIndexes);
211
    }
212
213
    /**
214
     * Execute the manipulation of solr documents
215
     *
216
     * @param SS_List $items
217
     * @param $type
218
     * @param BaseIndex $index
219
     * @return Result
220
     * @throws Exception
221
     */
222
    public function doManipulate($items, $type, BaseIndex $index): Result
223
    {
224
        $client = $index->getClient();
225
226
        $update = $this->getUpdate($items, $type, $index, $client);
227
228
        // commit immediately when in dev mode
229
230
        return $client->update($update);
231
    }
232
233
    /**
234
     * get the update object ready
235
     *
236
     * @param SS_List $items
237
     * @param string $type
238
     * @param BaseIndex $index
239
     * @param CoreClient $client
240
     * @return mixed
241
     * @throws Exception
242
     */
243
    protected function getUpdate($items, $type, BaseIndex $index, CoreClient $client)
244
    {
245
        // get an update query instance
246
        $update = $client->createUpdate();
247
248
        switch ($type) {
249
            case static::DELETE_TYPE:
250
                // By pushing to a single array, we have less memory usage and no duplicates
251
                // This is faster, and more efficient, because we only do one DB query
252
                $delete = $items->map('ID', 'ClassName')->toArray();
253
                array_walk($delete, static function (&$item, $key) {
254
                    $item = sprintf('%s-%s', $item, $key);
255
                });
256
                $update->addDeleteByIds(array_values($delete));
257
                // Remove the deletion array from memory
258
                break;
259
            case static::DELETE_TYPE_ALL:
260
                $update->addDeleteQuery('*:*');
261
                break;
262
            case static::UPDATE_TYPE:
263
            case static::CREATE_TYPE:
264
                $this->updateIndex($index, $items, $update);
265
                break;
266
        }
267
        $update->addCommit();
268
269
        return $update;
270
    }
271
272
    /**
273
     * Create the documents and add to the update
274
     *
275
     * @param BaseIndex $index
276
     * @param SS_List $items
277
     * @param Query $update
278
     * @throws Exception
279
     */
280
    public function updateIndex($index, $items, $update): void
281
    {
282
        $fields = $index->getFieldsForIndexing();
283
        $factory = $this->getFactory($items);
284
        $docs = $factory->buildItems($fields, $index, $update);
285
        if (count($docs)) {
286
            $update->addDocuments($docs);
287
        }
288
    }
289
290
    /**
291
     * Get the document factory prepared
292
     *
293
     * @param SS_List $items
294
     * @return DocumentFactory
295
     */
296
    protected function getFactory($items): DocumentFactory
297
    {
298
        $factory = Injector::inst()->get(DocumentFactory::class);
299
        $factory->setItems($items);
300
        $factory->setClass($items->first()->ClassName);
301
        $factory->setDebug($this->isDebug());
302
303
        return $factory;
304
    }
305
306
    /**
307
     * Check the Solr version to use
308
     * In version compare, we have the following results:
309
     *       1 means "result version is higher"
310
     *       0 means "result version is equal"
311
     *      -1 means "result version is lower"
312
     * We want to use the version "higher or equal to", because the
313
     * configs are for version X-and-up.
314
     * We loop through the versions available from high to low
315
     * therefore, if the version is lower, we want to check the next config version
316
     *
317
     * If no valid version is found, throw an error
318
     *
319
     * @param HandlerStack|null $handler Used for testing the solr version
320
     * @throws LogicException
321
     * @return int
322
     */
323
    public function getSolrVersion($handler = null): int
0 ignored issues
show
Unused Code introduced by
The parameter $handler is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

323
    public function getSolrVersion(/** @scrutinizer ignore-unused */ $handler = null): int

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
324
    {
325
        $query = $this->client->createApi([
326
            'handler' => 'admin/info/system',
327
            'version' => Request::API_V1,
328
        ]);
329
330
        $result = $this->client->execute($query)->getData();
331
332
        foreach (static::$solr_versions as $version) {
333
            $compare = version_compare($version, $result['lucene']['solr-spec-version']);
334
            if ($compare !== -1) {
335
                [$v] = explode('.', $version);
336
                return (int)$v;
337
            }
338
        }
339
340
        throw new LogicException('No valid version of Solr found!', 255);
341
    }
342
}
343