Passed
Push — sheepy/elevation-configuration ( f8fadb...77a4b0 )
by Marco
07:42
created

SolrCoreService::getSolrVersion()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 15
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 28
ccs 15
cts 15
cp 1
crap 4
rs 9.7666
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
     * Unique ID in Solr
52
     */
53
    const ID_FIELD = 'id';
54
    /**
55
     * SilverStripe ID of the object
56
     */
57
    const CLASS_ID_FIELD = 'ObjectID';
58
    /**
59
     * Name of the field that can be used for queries
60
     */
61
    const CLASSNAME = 'ClassName';
62
    /**
63
     * Solr update types
64
     */
65
    const DELETE_TYPE_ALL = 'deleteall';
66
    /**
67
     * string
68
     */
69
    const DELETE_TYPE = 'delete';
70
    /**
71
     * string
72
     */
73
    const UPDATE_TYPE = 'update';
74
    /**
75
     * string
76
     */
77
    const CREATE_TYPE = 'create';
78
79
    /**
80
     * @var array Base indexes that exist
81
     */
82
    protected $baseIndexes = [];
83
    /**
84
     * @var array Valid indexes out of the base indexes
85
     */
86
    protected $validIndexes = [];
87
88
    /**
89
     * SolrCoreService constructor.
90
     *
91
     * @throws ReflectionException
92
     */
93 85
    public function __construct()
94
    {
95 85
        $config = static::config()->get('config');
96 85
        $this->client = new Client($config);
97 85
        $this->client->setAdapter(new Guzzle());
98 85
        $this->admin = $this->client->createCoreAdmin();
99 85
        $this->baseIndexes = ClassInfo::subclassesFor(BaseIndex::class);
100 85
        $this->filterIndexes();
101 85
    }
102
103
    /**
104
     * Filter enabled indexes down to valid indexes that can be instantiated
105
     * or are allowed from config
106
     *
107
     * @throws ReflectionException
108
     */
109 85
    protected function filterIndexes(): void
110
    {
111 85
        $enabledIndexes = static::config()->get('indexes');
112 85
        $enabledIndexes = is_array($enabledIndexes) ? $enabledIndexes : $this->baseIndexes;
113 85
        foreach ($this->baseIndexes as $subindex) {
114
            // If the config of indexes is set, and the requested index isn't in it, skip addition
115
            // Or, the index simply doesn't exist, also a valid option
116 85
            if (!in_array($subindex, $enabledIndexes, true) ||
117 85
                !$this->checkReflection($subindex)
118
            ) {
119 85
                continue;
120
            }
121 85
            $this->validIndexes[] = $subindex;
122
        }
123 85
    }
124
125
    /**
126
     * Check if the class is instantiable
127
     *
128
     * @param $subindex
129
     * @return bool
130
     * @throws ReflectionException
131
     */
132 85
    protected function checkReflection($subindex): bool
133
    {
134 85
        $reflectionClass = new ReflectionClass($subindex);
135
136 85
        return $reflectionClass->isInstantiable();
137
    }
138
139
    /**
140
     * Update items in the list to Solr
141
     *
142
     * @param SS_List|DataObject $items
143
     * @param string $type
144
     * @param null|string $index
145
     * @return bool|Result
146
     * @throws ReflectionException
147
     * @throws Exception
148
     */
149 9
    public function updateItems($items, $type, $index = null)
150
    {
151 9
        $indexes = $this->getValidIndexes($index);
152
153 8
        $result = false;
154 8
        $items = ($items instanceof DataObject) ? ArrayList::create([$items]) : $items;
155 8
        $items = ($items instanceof SS_List) ? $items : ArrayList::create($items);
156
157 8
        $hierarchy = FieldResolver::getHierarchy($items->first()->ClassName);
158
159 8
        foreach ($indexes as $indexString) {
160
            /** @var BaseIndex $index */
161 8
            $index = Injector::inst()->get($indexString);
162 8
            $classes = $index->getClasses();
163 8
            $inArray = array_intersect($classes, $hierarchy);
164
            // No point in sending a delete|update|create for something that's not in the index
165 8
            if (!count($inArray)) {
166 7
                continue;
167
            }
168
169 8
            $result = $this->doManipulate($items, $type, $index);
170
        }
171
172 8
        return $result;
173
    }
174
175
    /**
176
     * Get valid indexes for the project
177
     *
178
     * @param null|string $index
179
     * @return array
180
     */
181 39
    public function getValidIndexes($index = null): array
182
    {
183 39
        if ($index && !in_array($index, $this->validIndexes, true)) {
184 1
            throw new LogicException('Incorrect index ' . $index);
185
        }
186
187 39
        if ($index) {
188 5
            return [$index];
189
        }
190
191
        // return the array values, to reset the keys
192 38
        return array_values($this->validIndexes);
193
    }
194
195
    /**
196
     * Execute the manipulation of solr documents
197
     *
198
     * @param SS_List $items
199
     * @param $type
200
     * @param BaseIndex $index
201
     * @return Result
202
     * @throws Exception
203
     */
204 10
    public function doManipulate($items, $type, BaseIndex $index): Result
205
    {
206 10
        $client = $index->getClient();
207
208 10
        $update = $this->getUpdate($items, $type, $index, $client);
209
210
        // commit immediately when in dev mode
211
212 10
        return $client->update($update);
213
    }
214
215
    /**
216
     * get the update object ready
217
     *
218
     * @param SS_List $items
219
     * @param string $type
220
     * @param BaseIndex $index
221
     * @param CoreClient $client
222
     * @return mixed
223
     * @throws Exception
224
     */
225 10
    protected function getUpdate($items, $type, BaseIndex $index, CoreClient $client)
226
    {
227
        // get an update query instance
228 10
        $update = $client->createUpdate();
229
230
        switch ($type) {
231 10
            case static::DELETE_TYPE:
232
                // By pushing to a single array, we have less memory usage and no duplicates
233
                // This is faster, and more efficient, because we only do one DB query
234 6
                $delete = $items->map('ID', 'ClassName')->toArray();
235 6
                array_walk($delete, static function (&$item, $key) {
236 6
                    $item = sprintf('%s-%s', $item, $key);
237 6
                });
238 6
                $update->addDeleteByIds(array_values($delete));
239
                // Remove the deletion array from memory
240 6
                break;
241 9
            case static::DELETE_TYPE_ALL:
242 2
                $update->addDeleteQuery('*:*');
243 2
                break;
244 7
            case static::UPDATE_TYPE:
245 1
            case static::CREATE_TYPE:
246 7
                $this->updateIndex($index, $items, $update);
247 7
                break;
248
        }
249 10
        $update->addCommit();
250
251 10
        return $update;
252
    }
253
254
    /**
255
     * Create the documents and add to the update
256
     *
257
     * @param BaseIndex $index
258
     * @param SS_List $items
259
     * @param Query $update
260
     * @throws Exception
261
     */
262 7
    public function updateIndex($index, $items, $update): void
263
    {
264 7
        $fields = $index->getFieldsForIndexing();
265 7
        $factory = $this->getFactory($items);
266 7
        $docs = $factory->buildItems($fields, $index, $update);
267 7
        if (count($docs)) {
268 7
            $update->addDocuments($docs);
269
        }
270 7
    }
271
272
    /**
273
     * Get the document factory prepared
274
     *
275
     * @param SS_List $items
276
     * @return DocumentFactory
277
     */
278 7
    protected function getFactory($items): DocumentFactory
279
    {
280 7
        $factory = Injector::inst()->get(DocumentFactory::class);
281 7
        $factory->setItems($items);
282 7
        $factory->setClass($items->first()->ClassName);
283 7
        $factory->setDebug($this->isDebug());
284
285 7
        return $factory;
286
    }
287
288
    /**
289
     * Check the Solr version to use
290
     *
291
     * @param HandlerStack|null $handler Used for testing the solr version
292
     * @return int
293
     */
294 36
    public function getSolrVersion($handler = null): int
295
    {
296 36
        $config = self::config()->get('config');
297 36
        $firstEndpoint = array_shift($config['endpoint']);
298
        $clientConfig = [
299 36
            'base_uri' => 'http://' . $firstEndpoint['host'] . ':' . $firstEndpoint['port'],
300
        ];
301
302 36
        if ($handler) {
303 1
            $clientConfig['handler'] = $handler;
304
        }
305
306 36
        $client = new GuzzleClient($clientConfig);
307
308 36
        $result = $client->get('solr/admin/info/system?wt=json');
309 36
        $result = json_decode($result->getBody(), 1);
310
311 36
        $return = 7;
312
        // Older than 5, newer than 7, a few new features added, only check if the version is still 7
313 36
        if (version_compare('6.9.9', $result['lucene']['solr-spec-version']) >= 0) {
314 1
            $return = 5;
315
        }
316
        // Old version 4
317 36
        if (version_compare('4.9.9', $result['lucene']['solr-spec-version']) >= 0) {
318 1
            $return = 4;
319
        }
320
321 36
        return $return;
322
    }
323
}
324