Passed
Push — bufferfix ( 8c6da4...848e8a )
by Simon
10:44
created

SolrCoreService::getUpdate()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

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