Passed
Push — hans/bufferadd ( 6f0d15...3149c2 )
by Simon
05:47
created

SolrCoreService::doUpdate()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

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