Completed
Push — master ( 4ef7f5...c8e8c2 )
by Juuso
03:51
created

AlgoliaManager   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 36
c 0
b 0
f 0
lcom 1
cbo 5
dl 0
loc 461
ccs 145
cts 145
cp 1
rs 8.8

20 Methods

Rating   Name   Duplication   Size   Complexity  
A removeMultipleFromIndices() 0 17 2
A __construct() 0 9 1
A getClient() 0 4 1
A setEnv() 0 4 1
A getEnv() 0 4 1
A pushToIndices() 0 12 2
A pushMultipleToIndices() 0 14 2
A updateInIndices() 0 13 2
A updateMultipleInIndices() 0 14 2
A removeFromIndices() 0 12 2
A reindex() 0 23 2
A reindexOnly() 0 13 2
B reindexByActiveQuery() 0 28 3
A clearIndices() 0 15 2
A search() 0 15 2
A __call() 0 4 1
A checkImplementsSearchableInterface() 0 8 2
A initIndices() 0 14 2
A getAlgoliaRecordsFromSearchableModelArray() 0 23 3
A reindexAtomically() 0 15 1
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
     * Initiates a new AlgoliaManager.
78
     *
79
     * @param Client $client
80
     * @param ActiveRecordFactory $activeRecordFactory
81
     * @param ActiveQueryChunker $activeQueryChunker
82
     */
83 56
    public function __construct(
84
        Client $client,
85
        ActiveRecordFactory $activeRecordFactory,
86
        ActiveQueryChunker $activeQueryChunker
87
    ) {
88 56
        $this->client = $client;
89 56
        $this->activeRecordFactory = $activeRecordFactory;
90 56
        $this->activeQueryChunker = $activeQueryChunker;
91 56
    }
92
93
    /**
94
     * Returns the Algolia Client.
95
     *
96
     * @return Client
97
     */
98 23
    public function getClient()
99
    {
100 23
        return $this->client;
101
    }
102
103
    /**
104
     * Sets the environment for the manager.
105
     *
106
     * @param string $env
107
     */
108 56
    public function setEnv($env)
109
    {
110 56
        $this->env = $env;
111 56
    }
112
113
    /**
114
     * Returns the environment for the manager.
115
     *
116
     * @return null|string
117
     */
118 1
    public function getEnv()
119
    {
120 1
        return $this->env;
121
    }
122
123
    /**
124
     * Indexes a searchable model to all indices.
125
     *
126
     * @param SearchableInterface $searchableModel
127
     *
128
     * @return array
129
     */
130 8
    public function pushToIndices(SearchableInterface $searchableModel)
131
    {
132 8
        $indices = $this->initIndices($searchableModel);
133 8
        $response = [];
134
135 8
        foreach ($indices as $index) {
136 8
            $record = $searchableModel->getAlgoliaRecord();
137 8
            $response[$index->indexName] = $index->addObject($record, $searchableModel->getObjectID());
138 8
        }
139
140 8
        return $response;
141
    }
142
143
    /**
144
     * Indexes multiple searchable models in a batch. The given searchable models must be of the same class.
145
     *
146
     * @param SearchableInterface[] $searchableModels
147
     *
148
     * @return array
149
     */
150 5
    public function pushMultipleToIndices(array $searchableModels)
151
    {
152 5
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
153 2
        $indices = $this->initIndices($searchableModels[0]);
154
155 2
        $response = [];
156
157 2
        foreach ($indices as $index) {
158
            /* @var Index $index  */
159 2
            $response[$index->indexName] = $index->addObjects($algoliaRecords);
160 2
        }
161
162 2
        return $response;
163
    }
164
165
    /**
166
     * Updates the models data in all indices.
167
     *
168
     * @param SearchableInterface $searchableModel
169
     *
170
     * @return array
171
     */
172 3
    public function updateInIndices(SearchableInterface $searchableModel)
173
    {
174 3
        $indices = $this->initIndices($searchableModel);
175 3
        $response = [];
176
177 3
        foreach ($indices as $index) {
178 3
            $record = $searchableModel->getAlgoliaRecord();
179 3
            $record['objectID'] = $searchableModel->getObjectID();
180 3
            $response[$index->indexName] = $index->saveObject($record);
181 3
        }
182
183 3
        return $response;
184
    }
185
186
    /**
187
     * Updates multiple models data in all indices. The given searchable models must be of the same class.
188
     *
189
     * @param SearchableInterface[] $searchableModels
190
     *
191
     * @return array
192
     */
193 4
    public function updateMultipleInIndices(array $searchableModels)
194
    {
195 4
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
196 2
        $indices = $this->initIndices($searchableModels[0]);
197
198 2
        $response = [];
199
200 2
        foreach ($indices as $index) {
201
            /* @var Index $index  */
202 2
            $response[$index->indexName] = $index->saveObjects($algoliaRecords);
203 2
        }
204
205 2
        return $response;
206
    }
207
208
    /**
209
     * Removes a searchable model from indices.
210
     *
211
     * @param SearchableInterface $searchableModel
212
     *
213
     * @return array
214
     * @throws \InvalidArgumentException
215
     */
216 3
    public function removeFromIndices(SearchableInterface $searchableModel)
217
    {
218 3
        $indices = $indices = $this->initIndices($searchableModel);
219 3
        $response = [];
220
221 3
        foreach ($indices as $index) {
222 3
            $objectID = $searchableModel->getObjectID();
223 3
            $response[$index->indexName] = $index->deleteObject($objectID);
224 3
        }
225
226 3
        return $response;
227
    }
228
229
    /**
230
     * Removes multiple models from all indices. The given searchable models must be of the same class.
231
     *
232
     * @param array $searchableModels
233
     *
234
     * @return array
235
     * @throws \InvalidArgumentException
236
     */
237 5
    public function removeMultipleFromIndices(array $searchableModels)
238
    {
239 5
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
240 2
        $indices = $this->initIndices($searchableModels[0]);
241
        $objectIds = array_map(function ($algoliaRecord) {
242 2
            return $algoliaRecord['objectID'];
243 2
        }, $algoliaRecords);
244
245 2
        $response = [];
246
247 2
        foreach ($indices as $index) {
248
            /* @var Index $index  */
249 2
            $response[$index->indexName] = $index->deleteObjects($objectIds);
250 2
        }
251
252 2
        return $response;
253
    }
254
255
    /**
256
     * Re-indexes the indices safely for the given ActiveRecord Class.
257
     *
258
     * @param string $className The name of the ActiveRecord to be indexed
259
     *
260
     * @return array
261
     */
262 3
    public function reindex($className)
263
    {
264 3
        $this->checkImplementsSearchableInterface($className);
265 2
        $activeRecord = $this->activeRecordFactory->make($className);
266
267 2
        $records = $this->activeQueryChunker->chunk(
268 2
            $activeRecord->find(),
269 2
            self::CHUNK_SIZE,
270
            function ($activeRecordEntities) {
271 2
                return $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
272
            }
273 2
        );
274
275
        /* @var SearchableInterface $activeRecord */
276 2
        $indices = $this->initIndices($activeRecord);
277 2
        $response = [];
278
279 2
        foreach ($indices as $index) {
280 2
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
281 2
        }
282
283 2
        return $response;
284
    }
285
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 3
    public function reindexOnly(array $searchableModels)
296
    {
297 3
        $records = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
298 1
        $indices = $this->initIndices($searchableModels[0]);
299
300 1
        $response = [];
301
302 1
        foreach ($indices as $index) {
303 1
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
304 1
        }
305
306 1
        return $response;
307
    }
308
309
    /**
310
     * Re-indexes the related indices for the given ActiveQueryInterface.
311
     * The result of the given ActiveQuery must consist from Searchable models of the same class.
312
     *
313
     * @param ActiveQueryInterface $activeQuery
314
     *
315
     * @return array
316
     */
317 4
    public function reindexByActiveQuery(ActiveQueryInterface $activeQuery)
318
    {
319 4
        $indices = null;
320
321 4
        $records = $this->activeQueryChunker->chunk(
322 4
            $activeQuery,
323 4
            self::CHUNK_SIZE,
324
            function ($activeRecordEntities) use (&$indices) {
325 4
                $records = $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
326
327
                // The converting ActiveRecords to Algolia ones already does the type checking
328
                // so it's safe to init indices here during the first chunk.
329 1
                if ($indices === null) {
330 1
                    $indices = $this->initIndices($activeRecordEntities[0]);
331 1
                }
332
333 1
                return $records;
334
            }
335 4
        );
336
337 1
        $response = [];
338
339 1
        foreach ($indices as $index) {
340 1
            $response[$index->indexName] = $this->reindexAtomically($index, $records);
341 1
        }
342
343 1
        return $response;
344
    }
345
346
    /**
347
     * Clears the indices for the given Class that implements SearchableInterface.
348
     *
349
     * @param string $className The name of the Class which indices are to be cleared.
350
     *
351
     * @throws \InvalidArgumentException
352
     * @return array
353
     */
354 2
    public function clearIndices($className)
355
    {
356 2
        $this->checkImplementsSearchableInterface($className);
357 1
        $activeRecord = $this->activeRecordFactory->make($className);
358 1
        $response = [];
359
360
        /* @var SearchableInterface $activeRecord */
361 1
        $indices = $indices = $this->initIndices($activeRecord);
362
363 1
        foreach ($indices as $index) {
364 1
            $response[$index->indexName] = $index->clearIndex();
365 1
        }
366
367 1
        return $response;
368
    }
369
370
    /**
371
     * @param string $className The name of the class which is to be searched.
372
     * @param string $query
373
     * @param array $searchParameters Optional search parameters given as an associative array.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $searchParameters not be null|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
374
     *
375
     * @link https://github.com/algolia/algoliasearch-client-php#search-parameters Allowed search parameters.
376
     *
377
     * @return array
378 20
     */
379
    public function search($className, $query, array $searchParameters = null)
380 20
    {
381
        $this->checkImplementsSearchableInterface($className);
382
        $activeRecord = $this->activeRecordFactory->make($className);
383
        $response = [];
384
385
        /* @var SearchableInterface $activeRecord */
386
        $indices = $indices = $this->initIndices($activeRecord);
387
388 22
        foreach ($indices as $index) {
389
            $response[$index->indexName] = $index->search($query, $searchParameters);
390 22
        }
391
392 22
        return $response;
393 6
    }
394
395 16
    /**
396
     * Dynamically pass methods to the Algolia Client.
397
     *
398
     * @param string $method
399
     * @param array $parameters
400
     *
401
     * @return mixed
402
     */
403
    public function __call($method, $parameters)
404 19
    {
405
        return call_user_func_array([$this->getClient(), $method], $parameters);
406 19
    }
407
408
    /**
409 19
     * Checks if the given class implements SearchableInterface.
410 3
     *
411 3
     * @param string $class Either name or instance of the class to be checked.
412
     */
413 19
    private function checkImplementsSearchableInterface($class)
414 19
    {
415
        $reflectionClass = new \ReflectionClass($class);
416 19
417
        if (! $reflectionClass->implementsInterface(SearchableInterface::class)) {
418
            throw new \InvalidArgumentException("The class: {$reflectionClass->name} doesn't implement leinonen\\Yii2Algolia\\SearchableInterface");
419
        }
420
    }
421
422
    /**
423
     * Initializes indices for the given SearchableModel.
424
     *
425
     * @param SearchableInterface $searchableModel
426 23
     *
427
     * @return Index[]
428 23
     */
429 4
    private function initIndices(SearchableInterface $searchableModel)
430
    {
431
        $indexNames = $searchableModel->getIndices();
432
433 19
        $indices = array_map(function ($indexName) {
434 19
            if ($this->env !== null) {
435
                $indexName = $this->env . '_' . $indexName;
436 15
            }
437 15
438 5
            return $this->initIndex($indexName);
439
        }, $indexNames);
440
441 15
        return $indices;
442 15
    }
443
444 15
    /**
445 15
     * Maps an array of searchable models into an Algolia friendly array.
446
     *
447 10
     * @param SearchableInterface[] $searchableModels
448
     *
449
     * @return array
450
     */
451
    private function getAlgoliaRecordsFromSearchableModelArray(array $searchableModels)
452
    {
453
        if (empty($searchableModels)) {
454
            throw new \InvalidArgumentException('The given array should not be empty');
455
        }
456
457
        // Use the first element of the array to define what kind of models we are indexing.
458 4
        $arrayType = get_class($searchableModels[0]);
459
        $this->checkImplementsSearchableInterface($arrayType);
460 4
461
        $algoliaRecords = array_map(function (SearchableInterface $searchableModel) use ($arrayType) {
462 4
            if (! $searchableModel instanceof $arrayType) {
463 4
                throw new \InvalidArgumentException('The given array should not contain multiple different classes');
464
            }
465 4
466
            $algoliaRecord = $searchableModel->getAlgoliaRecord();
467
            $algoliaRecord['objectID'] = $searchableModel->getObjectID();
468
469 4
            return $algoliaRecord;
470
        }, $searchableModels);
471 4
472
        return $algoliaRecords;
473
    }
474
475
    /**
476
     * Reindex atomically the given index with the given records.
477
     *
478
     * @param Index $index
479
     * @param array $algoliaRecords
480
     *
481
     * @return mixed
482
     */
483
    private function reindexAtomically(Index $index, array $algoliaRecords)
484
    {
485
        $temporaryIndexName = 'tmp_' . $index->indexName;
486
487
        $temporaryIndex = $this->initIndex($temporaryIndexName);
488
        $temporaryIndex->addObjects($algoliaRecords);
489
490
        $settings = $index->getSettings();
491
492
        // Temporary index overrides all the settings on the main one.
493
        // So we need to set the original settings on the temporary one before atomically moving the index.
494
        $temporaryIndex->setSettings($settings);
495
496
        return $this->moveIndex($temporaryIndexName, $index->indexName);
497
    }
498
499
}
500