Passed
Push — hans/core-extraction ( a97b4e...a44af8 )
by Simon
06:16
created

SolrCoreService::getValidClasses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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