Passed
Pull Request — master (#225)
by Simon
21:24 queued 05:14
created

SolrCoreService::getSolrAuthentication()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 14
ccs 6
cts 6
cp 1
crap 3
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\SolrSearch\Services
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 LogicException;
21
use ReflectionClass;
22
use ReflectionException;
23
use SilverStripe\Core\ClassInfo;
24
use SilverStripe\Core\Config\Configurable;
25
use SilverStripe\Core\Injector\Injectable;
26
use SilverStripe\Core\Injector\Injector;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataObject;
29
use SilverStripe\ORM\SS_List;
30
use Solarium\Client;
31
use Solarium\Core\Client\Adapter\Guzzle;
32
use Solarium\Core\Client\Client as CoreClient;
33
use Solarium\QueryType\Update\Query\Query;
34
use Solarium\QueryType\Update\Result;
35
36
/**
37
 * Class SolrCoreService provides the base connection to Solr.
38
 *
39
 * Default service to connect to Solr and handle all base requirements to support Solr.
40
 * Default constants are available to support any set up.
41
 *
42
 * @package Firesphere\SolrSearch\Services
43
 */
44
class SolrCoreService
45
{
46
    use Injectable;
47
    use Configurable;
48
    use CoreServiceTrait;
49
    use CoreAdminTrait;
50
51
    /**
52
     * Unique ID in Solr
53
     */
54
    const ID_FIELD = 'id';
55
    /**
56
     * SilverStripe ID of the object
57
     */
58
    const CLASS_ID_FIELD = 'ObjectID';
59
    /**
60
     * Name of the field that can be used for queries
61
     */
62
    const CLASSNAME = 'ClassName';
63
    /**
64
     * Solr update types
65
     */
66
    const DELETE_TYPE_ALL = 'deleteall';
67
    /**
68
     * string
69
     */
70
    const DELETE_TYPE = 'delete';
71
    /**
72
     * string
73
     */
74
    const UPDATE_TYPE = 'update';
75
    /**
76
     * string
77
     */
78
    const CREATE_TYPE = 'create';
79
80
    /**
81
     * @var array Base indexes that exist
82
     */
83
    protected $baseIndexes = [];
84
    /**
85
     * @var array Valid indexes out of the base indexes
86
     */
87
    protected $validIndexes = [];
88
89
    /**
90
     * SolrCoreService constructor.
91
     *
92
     * @throws ReflectionException
93
     */
94 88
    public function __construct()
95
    {
96 88
        $config = static::config()->get('config');
97 88
        $this->client = new Client($config);
98 88
        $this->client->setAdapter(new Guzzle());
0 ignored issues
show
Deprecated Code introduced by
The class Solarium\Core\Client\Adapter\Guzzle has been deprecated: Deprecated since Solarium 5.2 and will be removed in Solarium 6. Use Psr18Adapter instead. ( Ignorable by Annotation )

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

98
        $this->client->setAdapter(/** @scrutinizer ignore-deprecated */ new Guzzle());
Loading history...
99 88
        $this->admin = $this->client->createCoreAdmin();
100 88
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
101 88
        $this->filterIndexes();
102 88
    }
103
104
    /**
105
     * Filter enabled indexes down to valid indexes that can be instantiated
106
     * or are allowed from config
107
     *
108
     * @throws ReflectionException
109
     */
110 88
    protected function filterIndexes(): void
111
    {
112 88
        $enabledIndexes = static::config()->get('indexes');
113 88
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
114 88
        foreach ($this->baseIndexes as $subindex) {
115
            // If the config of indexes is set, and the requested index isn't in it, skip addition
116
            // Or, the index simply doesn't exist, also a valid option
117 88
            if (!in_array($subindex, $enabledIndexes, true) ||
118 88
                !$this->checkReflection($subindex)
119
            ) {
120 88
                continue;
121
            }
122 88
            $this->validIndexes[] = $subindex;
123
        }
124 88
    }
125
126
    /**
127
     * Check if the class is instantiable
128
     *
129
     * @param $subindex
130
     * @return bool
131
     * @throws ReflectionException
132
     */
133 88
    protected function checkReflection($subindex): bool
134
    {
135 88
        $reflectionClass = new ReflectionClass($subindex);
136
137 88
        return $reflectionClass->isInstantiable();
138
    }
139
140
    /**
141
     * Update items in the list to Solr
142
     *
143
     * @param SS_List|DataObject $items
144
     * @param string $type
145
     * @param null|string $index
146
     * @return bool|Result
147
     * @throws ReflectionException
148
     * @throws Exception
149
     */
150 9
    public function updateItems($items, $type, $index = null)
151
    {
152 9
        $indexes = $this->getValidIndexes($index);
153
154 8
        $result = false;
155 8
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
156 8
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
157
158 8
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
159
160 8
        foreach ($indexes as $indexString) {
161
            /** @var BaseIndex $index */
162 8
            $index = Injector::inst()->get($indexString);
163 8
            $classes = $index->getClasses();
164 8
            $inArray = array_intersect($classes, $hierarchy);
165
            // No point in sending a delete|update|create for something that's not in the index
166 8
            if (!count($inArray)) {
167 7
                continue;
168
            }
169
170 8
            $result = $this->doManipulate($items, $type, $index);
171
        }
172
173 8
        return $result;
174
    }
175
176
    /**
177
     * Get valid indexes for the project
178
     *
179
     * @param null|string $index
180
     * @return array
181
     */
182 42
    public function getValidIndexes($index = null): array
183
    {
184 42
        if ($index && !in_array($index, $this->validIndexes, true)) {
185 1
            throw new LogicException('Incorrect index ' . $index);
186
        }
187
188 42
        if ($index) {
189 5
            return [$index];
190
        }
191
192
        // return the array values, to reset the keys
193 41
        return array_values($this->validIndexes);
194
    }
195
196
    /**
197
     * Execute the manipulation of solr documents
198
     *
199
     * @param SS_List $items
200
     * @param $type
201
     * @param BaseIndex $index
202
     * @return Result
203
     * @throws Exception
204
     */
205 10
    public function doManipulate($items, $type, BaseIndex $index): Result
206
    {
207 10
        $client = $index->getClient();
208
209 10
        $update = $this->getUpdate($items, $type, $index, $client);
210
211
        // commit immediately when in dev mode
212
213 10
        return $client->update($update);
214
    }
215
216
    /**
217
     * get the update object ready
218
     *
219
     * @param SS_List $items
220
     * @param string $type
221
     * @param BaseIndex $index
222
     * @param CoreClient $client
223
     * @return mixed
224
     * @throws Exception
225
     */
226 10
    protected function getUpdate($items, $type, BaseIndex $index, CoreClient $client)
227
    {
228
        // get an update query instance
229 10
        $update = $client->createUpdate();
230
231
        switch ($type) {
232 10
            case static::DELETE_TYPE:
233
                // By pushing to a single array, we have less memory usage and no duplicates
234
                // This is faster, and more efficient, because we only do one DB query
235 6
                $delete = $items->map('ID', 'ClassName')->toArray();
236 6
                array_walk($delete, static function (&$item, $key) {
237 6
                    $item = sprintf('%s-%s', $item, $key);
238 6
                });
239 6
                $update->addDeleteByIds(array_values($delete));
240
                // Remove the deletion array from memory
241 6
                break;
242 9
            case static::DELETE_TYPE_ALL:
243 2
                $update->addDeleteQuery('*:*');
244 2
                break;
245 7
            case static::UPDATE_TYPE:
246 1
            case static::CREATE_TYPE:
247 7
                $this->updateIndex($index, $items, $update);
248 7
                break;
249
        }
250 10
        $update->addCommit();
251
252 10
        return $update;
253
    }
254
255
    /**
256
     * Create the documents and add to the update
257
     *
258
     * @param BaseIndex $index
259
     * @param SS_List $items
260
     * @param Query $update
261
     * @throws Exception
262
     */
263 7
    public function updateIndex($index, $items, $update): void
264
    {
265 7
        $fields = $index->getFieldsForIndexing();
266 7
        $factory = $this->getFactory($items);
267 7
        $docs = $factory->buildItems($fields, $index, $update);
268 7
        if (count($docs)) {
269 7
            $update->addDocuments($docs);
270
        }
271 7
    }
272
273
    /**
274
     * Get the document factory prepared
275
     *
276
     * @param SS_List $items
277
     * @return DocumentFactory
278
     */
279 7
    protected function getFactory($items): DocumentFactory
280
    {
281 7
        $factory = Injector::inst()->get(DocumentFactory::class);
282 7
        $factory->setItems($items);
283 7
        $factory->setClass($items->first()->ClassName);
284 7
        $factory->setDebug($this->isDebug());
285
286 7
        return $factory;
287
    }
288
289
    /**
290
     * Check the Solr version to use
291
     *
292
     * @param HandlerStack|null $handler Used for testing the solr version
293
     * @return int
294
     */
295 37
    public function getSolrVersion($handler = null): int
296
    {
297 37
        $config = self::config()->get('config');
298 37
        $firstEndpoint = array_shift($config['endpoint']);
299
        $clientConfig = [
300 37
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
301
        ];
302
303 37
        if ($handler) {
304 1
            $clientConfig['handler'] = $handler;
305
        }
306
307 37
        $clientOptions = $this->getSolrAuthentication($firstEndpoint);
308
309 37
        $client = new GuzzleClient($clientConfig);
310
311 37
        $result = $client->get('solr/admin/info/system?wt=json', $clientOptions);
312 37
        $result = json_decode($result->getBody(), 1);
313
314 37
        $version = 7;
315
        // Newer than 4, older than 7, a few new features added
316 37
        if (version_compare('6.9.9', $result['lucene']['solr-spec-version']) >= 0) {
317 1
            $version = 5;
318
        }
319
        // Old version 4
320 37
        if (version_compare('4.9.9', $result['lucene']['solr-spec-version']) >= 0) {
321 1
            $version = 4;
322
        }
323
324 37
        return $version;
325
    }
326
327
    /**
328
     * This will add the authentication headers to the request.
329
     * It's intended to become a helper in the end.
330
     *
331
     * @param array $firstEndpoint
332
     * @return array|array[]
333
     */
334 37
    private function getSolrAuthentication($firstEndpoint): array
335
    {
336 37
        $clientOptions = [];
337
338 37
        if (isset($firstEndpoint['username']) && isset($firstEndpoint['password'])) {
339
            $clientOptions = [
340
                'auth' => [
341 37
                    $firstEndpoint['username'],
342 37
                    $firstEndpoint['password']
343
                ]
344
            ];
345
        }
346
347 37
        return $clientOptions;
348
    }
349
}
350