Completed
Push — master ( 9261a2...50f625 )
by Juuso
08:29
created

AlgoliaManager::reindexAtomically()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 2
crap 2
1
<?php
2
3
namespace leinonen\Yii2Algolia;
4
5
use AlgoliaSearch\Client;
6
use AlgoliaSearch\Index;
7
use leinonen\Yii2Algolia\ActiveRecord\ActiveQueryChunker;
8
use leinonen\Yii2Algolia\ActiveRecord\ActiveRecordFactory;
9
use yii\db\ActiveQueryInterface;
10
11
/**
12
 * @method setConnectTimeout(int $connectTimeout, int $timeout = 30, int $searchTimeout = 5)
13
 * @method enableRateLimitForward(string $adminAPIKey, string $endUserIP, string $rateLimitAPIKey)
14
 * @method setForwarderFor(string $ip)
15
 * @method setAlgoliaUserToken(string $token)
16
 * @method disableRateLimitForward()
17
 * @method isAlive()
18
 * @method setExtraHeader(string $key, string $value)
19
 * @method mixed multipleQueries(array $queries, string $indexNameKey = "indexName", string $strategy = "none")
20
 * @method mixed listIndexes()
21
 * @method deleteIndex(string $indexName)
22
 * @method mixed moveIndex(string $srcIndexName, string $dstIndexName)
23
 * @method mixed copyIndex(string $srcIndexName, string $dstIndexName)
24
 * @method mixed getLogs(int $offset = 0, int $length = 10, string $type = "all")
25
 * @method Index initIndex(string $indexName)
26
 * @method mixed listUserKeys()
27
 * @method mixed getUserKeyACL(string $key)
28
 * @method mixed deleteUserKey(string $key)
29
 * @method mixed addUserKey(array $obj, int $validity = 0, int $maxQueriesPerIPPerHour = 0, int $maxHitsPerQuery = 0, array $indexes = null)
30
 * @method mixed updateUserKey(string $key, array $obj, int $validity = 0, int $maxQueriesPerIPPerHour = 0, int $maxHitsPerQuery = 0, array $indexes = null)
31
 * @method mixed batch(array $requests)
32
 * @method string generateSecuredApiKey(string $privateApiKey, mixed $query, string $userToken = null)
33
 * @method string buildQuery(array $args)
34
 * @method mixed request(\AlgoliaSearch\ClientContext $context, string $method, string $path, array $params, array $data, array $hostsArray, int $connectTimeout, int $readTimeout)
35
 * @method mixed doRequest(\AlgoliaSearch\ClientContext $context, string $method, string $path, array $params, array $data, array $hostsArray, int $connectTimeout, int $readTimeout)
36
 * @method \AlgoliaSearch\PlacesIndex initPlaces(string $appId, string $appKey, array $hostsArray = null, array $options = [])
37
 * @see Client
38
 */
39
class AlgoliaManager
40
{
41
    /**
42
     * Size for the chunks used in reindexing methods
43
     */
44
     const CHUNK_SIZE = 500;
45
46
    /**
47
     * @var AlgoliaFactory
48
     */
49
    protected $factory;
50
51
    /**
52
     * @var AlgoliaConfig
53
     */
54
    protected $config;
55
56
    /**
57
     * @var Client
58
     */
59
    protected $client;
60
61
    /**
62
     * @var ActiveRecordFactory
63
     */
64
    protected $activeRecordFactory;
65
66
    /**
67
     * @var null|string
68
     */
69
    protected $env;
70
71
    /**
72
     * @var ActiveQueryChunker
73
     */
74
    private $activeQueryChunker;
75
76
    /**
77 39
     * Initiates a new AlgoliaManager.
78
     *
79
     * @param Client $client
80
     * @param ActiveRecordFactory $activeRecordFactory
81
     * @param ActiveQueryChunker $activeQueryChunker
82 39
     */
83 39
    public function __construct(
84 39
        Client $client,
85 39
        ActiveRecordFactory $activeRecordFactory,
86
        ActiveQueryChunker $activeQueryChunker
87
    ) {
88
        $this->client = $client;
89
        $this->activeRecordFactory = $activeRecordFactory;
90
        $this->activeQueryChunker = $activeQueryChunker;
91
    }
92 17
93
    /**
94 17
     * Returns the Algolia Client.
95
     *
96
     * @return Client
97
     */
98
    public function getClient()
99
    {
100
        return $this->client;
101
    }
102 39
103
    /**
104 39
     * Sets the environment for the manager.
105 39
     *
106
     * @param string $env
107
     */
108
    public function setEnv($env)
109
    {
110
        $this->env = $env;
111
    }
112 1
113
    /**
114 1
     * Returns the environment for the manager.
115
     *
116
     * @return null|string
117
     */
118
    public function getEnv()
119
    {
120
        return $this->env;
121
    }
122
123
    /**
124 5
     * Indexes a searchable model to all indices.
125
     *
126 5
     * @param SearchableInterface $searchableModel
127 5
     *
128
     * @return array
129 5
     */
130 5
    public function pushToIndices(SearchableInterface $searchableModel)
131 5
    {
132 5
        $indices = $this->initIndices($searchableModel);
133
        $response = [];
134 5
135
        foreach ($indices as $index) {
136
            $record = $searchableModel->getAlgoliaRecord();
137
            $response[$index->indexName] = $index->addObject($record, $searchableModel->getObjectID());
138
        }
139
140
        return $response;
141
    }
142
143
    /**
144 3
     * Indexes multiple searchable models in a batch. The given searchable models must be of the same class.
145
     *
146 3
     * @param SearchableInterface[] $searchableModels
147 2
     *
148
     * @return array
149 2
     */
150
    public function pushMultipleToIndices(array $searchableModels)
151 2
    {
152
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
153 2
        $indices = $this->initIndices($searchableModels[0]);
154 2
155
        $response = [];
156 2
157
        foreach ($indices as $index) {
158
            /* @var Index $index  */
159
            $response[$index->indexName] = $index->addObjects($algoliaRecords);
160
        }
161
162
        return $response;
163
    }
164
165
    /**
166 3
     * Updates the models data in all indices.
167
     *
168 3
     * @param SearchableInterface $searchableModel
169 3
     *
170
     * @return array
171 3
     */
172 3
    public function updateInIndices(SearchableInterface $searchableModel)
173 3
    {
174 3
        $indices = $this->initIndices($searchableModel);
175 3
        $response = [];
176
177 3
        foreach ($indices as $index) {
178
            $record = $searchableModel->getAlgoliaRecord();
179
            $record['objectID'] = $searchableModel->getObjectID();
180
            $response[$index->indexName] = $index->saveObject($record);
181
        }
182
183
        return $response;
184
    }
185
186
    /**
187 2
     * Updates multiple models data in all indices. The given searchable models must be of the same class.
188
     *
189 2
     * @param SearchableInterface[] $searchableModels
190 1
     *
191
     * @return array
192 1
     */
193
    public function updateMultipleInIndices(array $searchableModels)
194 1
    {
195
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
196 1
        $indices = $this->initIndices($searchableModels[0]);
197 1
198
        $response = [];
199 1
200
        foreach ($indices as $index) {
201
            /* @var Index $index  */
202
            $response[$index->indexName] = $index->saveObjects($algoliaRecords);
203
        }
204
205
        return $response;
206
    }
207
208
    /**
209
     * Removes a searchable model from indices.
210 2
     *
211
     * @param SearchableInterface $searchableModel
212 2
     *
213 2
     * @return array
214
     * @throws \InvalidArgumentException
215 2
     */
216 2
    public function removeFromIndices(SearchableInterface $searchableModel)
217 2
    {
218 2
        $indices = $indices = $this->initIndices($searchableModel);
219
        $response = [];
220 2
221
        foreach ($indices as $index) {
222
            $objectID = $searchableModel->getObjectID();
223
            $response[$index->indexName] = $index->deleteObject($objectID);
224
        }
225
226
        return $response;
227
    }
228
229
    /**
230 3
     * Removes multiple models from all indices. The given searchable models must be of the same class.
231
     *
232 3
     * @param array $searchableModels
233 2
     *
234 2
     * @return array
235
     * @throws \InvalidArgumentException
236 2
     */
237 2
    public function removeMultipleFromIndices(array $searchableModels)
238 2
    {
239
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
240 2
        $indices = $this->initIndices($searchableModels[0]);
241
        $objectIds = array_map(function ($algoliaRecord) {
242 2
            return $algoliaRecord['objectID'];
243
        }, $algoliaRecords);
244 2
245
        $response = [];
246 2
247 2
        foreach ($indices as $index) {
248
            /* @var Index $index  */
249
            $response[$index->indexName] = $index->deleteObjects($objectIds);
250 2
        }
251 2
252
        return $response;
253 2
    }
254
255
    /**
256
     * Re-indexes the indices safely for the given ActiveRecord Class.
257 2
     *
258
     * @param string $className The name of the ActiveRecord to be indexed
259 2
     *
260 2
     * @return array
261
     */
262 2
    public function reindex($className)
263
    {
264
        $this->checkImplementsSearchableInterface($className);
265
        $activeRecord = $this->activeRecordFactory->make($className);
266
267
        $records = $this->activeQueryChunker->chunk(
268
            $activeRecord->find(),
269
            self::CHUNK_SIZE,
270
            function ($activeRecordEntities) {
271
                return $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
272 2
            }
273
        );
274 2
275 1
        /** @var SearchableInterface $activeRecord */
276 1
        $indices = $this->initIndices($activeRecord);
277
        $response = [];
278
279 1
        foreach ($indices as $index) {
280
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
281 1
        }
282 1
283 1
        return $response;
284
    }
285 1
286
    /**
287
     * Re-indexes the related indices for the given array only with the objects from the given array.
288
     * The given array must consist of Searchable objects of same class.
289
     *
290
     * @param SearchableInterface[] $searchableModels
291
     *
292
     * @throws \InvalidArgumentException
293
     * @return array
294
     */
295
    public function reindexOnly(array $searchableModels)
296 14
    {
297
        $records = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
298 14
        $indices = $this->initIndices($searchableModels[0]);
299
300
        $response = [];
301
302
        foreach ($indices as $index) {
303
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
304
        }
305
306 5
        return $response;
307
    }
308 5
309
    /**
310 5
     * Re-indexes the related indices for the given ActiveQueryInterface.
311 2
     * The result of the given ActiveQuery must consist from Searchable models of the same class.
312
     *
313 3
     * @param ActiveQueryInterface $activeQuery
314
     *
315
     * @return array
316
     */
317
    public function reindexByActiveQuery(ActiveQueryInterface $activeQuery)
318
    {
319
        $indices = null;
320
321
        $records = $this->activeQueryChunker->chunk(
322 13
            $activeQuery,
323
            self::CHUNK_SIZE,
324 13
            function ($activeRecordEntities) use (&$indices){
325
                $records = $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
326
327 13
                // The converting ActiveRecords to Algolia ones already does the type checking
328 3
                // so it's safe to init indices here during the first chunk.
329 3
                if($indices === null) {
330
                    $indices = $this->initIndices($activeRecordEntities[0]);
331 13
                }
332 13
333
                return $records;
334 13
            }
335
        );
336
337
        $response = [];
338
339
        foreach ($indices as $index) {
1 ignored issue
show
Bug introduced by
The expression $indices of type null is not traversable.
Loading history...
340
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
341
        }
342
343
        return $response;
344 7
    }
345
346
    /**
347 7
     * Clears the indices for the given Class that implements SearchableInterface.
348
     *
349 7
     * @param string $className The name of the Class which indices are to be cleared.
350 7
     *
351 2
     * @throws \InvalidArgumentException
352
     * @return array
353
     */
354 7
    public function clearIndices($className)
355 7
    {
356
        $this->checkImplementsSearchableInterface($className);
357 7
        $activeRecord = $this->activeRecordFactory->make($className);
358 7
        $response = [];
359
360 5
        /* @var SearchableInterface $activeRecord */
361
        $indices = $indices = $this->initIndices($activeRecord);
362
363
        foreach ($indices as $index) {
364
            $response[$index->indexName] = $index->clearIndex();
365
        }
366
367
        return $response;
368
    }
369
370
    /**
371
     * Dynamically pass methods to the Algolia Client.
372
     *
373
     * @param string $method
374
     * @param array $parameters
375
     *
376
     * @return mixed
377
     */
378
    public function __call($method, $parameters)
379
    {
380
        return call_user_func_array([$this->getClient(), $method], $parameters);
381
    }
382
383
    /**
384
     * Checks if the given class implements SearchableInterface.
385
     *
386
     * @param string $class Either name or instance of the class to be checked.
387
     */
388
    private function checkImplementsSearchableInterface($class)
389
    {
390
        $reflectionClass = new \ReflectionClass($class);
391
392
        if (! $reflectionClass->implementsInterface(SearchableInterface::class)) {
393
            throw new \InvalidArgumentException("The class: {$reflectionClass->name} doesn't implement leinonen\\Yii2Algolia\\SearchableInterface");
394
        }
395
    }
396
397
    /**
398
     * Initializes indices for the given SearchableModel.
399
     *
400
     * @param SearchableInterface $searchableModel
401
     *
402
     * @return Index[]
403
     */
404
    private function initIndices(SearchableInterface $searchableModel)
405
    {
406
        $indexNames = $searchableModel->getIndices();
407
408
        $indices = array_map(function ($indexName) {
409
            if ($this->env !== null) {
410
                $indexName = $this->env . '_' . $indexName;
411
            }
412
413
            return $this->initIndex($indexName);
414
        }, $indexNames);
415
416
        return $indices;
417
    }
418
419
    /**
420
     * Maps an array of searchable models into an Algolia friendly array.
421
     *
422
     * @param SearchableInterface[] $searchableModels
423
     *
424
     * @return array
425
     */
426
    private function getAlgoliaRecordsFromSearchableModelArray(array $searchableModels)
427
    {
428
        if(empty($searchableModels)) {
429
            throw new \InvalidArgumentException('The given array should not be empty');
430
        }
431
432
        // Use the first element of the array to define what kind of models we are indexing.
433
        $arrayType = get_class($searchableModels[0]);
434
435
        $algoliaRecords = array_map(function (SearchableInterface $searchableModel) use ($arrayType) {
436
            if (! $searchableModel instanceof $arrayType) {
437
                throw new \InvalidArgumentException('The given array should not contain multiple different classes');
438
            }
439
440
            $algoliaRecord = $searchableModel->getAlgoliaRecord();
441
            $algoliaRecord['objectID'] = $searchableModel->getObjectID();
442
443
            return $algoliaRecord;
444
        }, $searchableModels);
445
446
        return $algoliaRecords;
447
    }
448
449
    /**
450
     * Reindex atomically the given index with the given records.
451
     *
452
     * @param Index $index
453
     * @param array $algoliaRecords
454
     *
455
     * @return mixed
456
     */
457
    private function reindexAtomically(Index $index, array $algoliaRecords)
458
    {
459
        $temporaryIndexName = 'tmp_' . $index->indexName;
460
461
        $temporaryIndex = $this->initIndex($temporaryIndexName);
462
        $temporaryIndex->addObjects($algoliaRecords);
463
464
        $settings = $index->getSettings();
465
466
        // Temporary index overrides all the settings on the main one.
467
        // So we need to set the original settings on the temporary one before atomically moving the index.
468
        $temporaryIndex->setSettings($settings);
469
470
        return $this->moveIndex($temporaryIndexName, $index->indexName);
471
    }
472
}
473