getAlgoliaRecordsFromSearchableModelArray()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 8
cts 8
cp 1
rs 9.3142
cc 3
eloc 12
nc 2
nop 1
crap 3
1
<?php
2
3
namespace leinonen\Yii2Algolia;
4
5
use AlgoliaSearch\Index;
6
use AlgoliaSearch\Client;
7
use leinonen\Yii2Algolia\ActiveRecord\Searchable;
8
use yii\db\ActiveQueryInterface;
9
use leinonen\Yii2Algolia\ActiveRecord\ActiveQueryChunker;
10
use leinonen\Yii2Algolia\ActiveRecord\ActiveRecordFactory;
11
12
/**
13
 * @method setConnectTimeout(int $connectTimeout, int $timeout = 30, int $searchTimeout = 5)
14
 * @method enableRateLimitForward(string $adminAPIKey, string $endUserIP, string $rateLimitAPIKey)
15
 * @method setForwarderFor(string $ip)
16
 * @method setAlgoliaUserToken(string $token)
17
 * @method disableRateLimitForward()
18
 * @method isAlive()
19
 * @method setExtraHeader(string $key, string $value)
20
 * @method mixed multipleQueries(array $queries, string $indexNameKey = "indexName", string $strategy = "none")
21
 * @method mixed listIndexes()
22
 * @method deleteIndex(string $indexName)
23
 * @method mixed moveIndex(string $srcIndexName, string $dstIndexName)
24
 * @method mixed copyIndex(string $srcIndexName, string $dstIndexName)
25
 * @method scopedCopyIndex(string $srcIndexName, string $dstIndexName, array $scope = [], array $requestHeaders = [])
26
 * @method mixed getLogs(int $offset = 0, int $length = 10, string $type = "all")
27
 * @method assignUserID($userID, $clusterName)
28
 * @method removeUserID($userID)
29
 * @method listClusters()
30
 * @method getUserID($userID)
31
 * @method listUserIDs($page = 0, $hitsPerPage = 20)
32
 * @method getTopUserID()
33
 * @method searchUserIDs($query, $clusterName = null, $page = null, $hitsPerPage = null)
34
 * @method Index initIndex(string $indexName)
35
 * @method mixed listApiKeys()
36
 * @method mixed getApiKey(string $key)
37
 * @method mixed deleteApiKey(string $key)
38
 * @method mixed addApiKey(array $obj, int $validity = 0, int $maxQueriesPerIPPerHour = 0, int $maxHitsPerQuery = 0, array $indexes = null)
39
 * @method mixed updateApiKey(string $key, array $obj, int $validity = 0, int $maxQueriesPerIPPerHour = 0, int $maxHitsPerQuery = 0, array $indexes = null)
40
 * @method mixed batch(array $requests)
41
 * @method string generateSecuredApiKey(string $privateApiKey, mixed $query, string $userToken = null)
42
 * @method string buildQuery(array $args)
43
 * @method mixed request(\AlgoliaSearch\ClientContext $context, string $method, string $path, array $params, array $data, array $hostsArray, int $connectTimeout, int $readTimeout)
44
 * @method mixed doRequest(\AlgoliaSearch\ClientContext $context, string $method, string $path, array $params, array $data, array $hostsArray, int $connectTimeout, int $readTimeout)
45
 * @method \AlgoliaSearch\PlacesIndex initPlaces(string $appId = null, string $appKey = null, array $hostsArray = null, array $options = [])
46
 * @method getContext()
47
 *
48
 * @see Client
49
 */
50
class AlgoliaManager
51
{
52
    /**
53
      * Size for the chunks used in reindexing methods.
54
      */
55
     const CHUNK_SIZE = 500;
56
57
    /**
58
     * @var AlgoliaFactory
59
     */
60
    protected $factory;
61
62
    /**
63
     * @var AlgoliaConfig
64
     */
65
    protected $config;
66
67
    /**
68
     * @var Client
69
     */
70
    protected $client;
71
72
    /**
73
     * @var ActiveRecordFactory
74
     */
75
    protected $activeRecordFactory;
76
77
    /**
78
     * @var null|string
79
     */
80
    protected $env;
81
82
    /**
83 65
     * @var ActiveQueryChunker
84
     */
85
    private $activeQueryChunker;
86
87
    /**
88 65
     * Initiates a new AlgoliaManager.
89 65
     *
90 65
     * @param Client $client
91 65
     * @param ActiveRecordFactory $activeRecordFactory
92
     * @param ActiveQueryChunker $activeQueryChunker
93
     */
94
    public function __construct(
95
        Client $client,
96
        ActiveRecordFactory $activeRecordFactory,
97
        ActiveQueryChunker $activeQueryChunker
98 29
    ) {
99
        $this->client = $client;
100 29
        $this->activeRecordFactory = $activeRecordFactory;
101
        $this->activeQueryChunker = $activeQueryChunker;
102
    }
103
104
    /**
105
     * Returns the Algolia Client.
106
     *
107
     * @return Client
108 65
     */
109
    public function getClient()
110 65
    {
111 65
        return $this->client;
112
    }
113
114
    /**
115
     * Sets the environment for the manager.
116
     *
117
     * @param string $env
118 1
     */
119
    public function setEnv($env)
120 1
    {
121
        $this->env = $env;
122
    }
123
124
    /**
125
     * Returns the environment for the manager.
126
     *
127
     * @return null|string
128
     */
129
    public function getEnv()
130 10
    {
131
        return $this->env;
132 10
    }
133 10
134
    /**
135 10
     * Indexes a searchable model to all indices.
136 10
     *
137 10
     * @param SearchableInterface $searchableModel
138 10
     *
139
     * @return array
140 10
     */
141
    public function pushToIndices(SearchableInterface $searchableModel)
142
    {
143
        $indices = $this->initIndices($searchableModel);
144
        $record = $searchableModel->getAlgoliaRecord();
145
146
        return $this->processIndices($indices, function (Index $index) use ($record, $searchableModel) {
147
            return $index->addObject($record, $searchableModel->getObjectID());
148
        });
149
    }
150 5
151
    /**
152 5
     * Indexes multiple searchable models in a batch. The given searchable models must be of the same class.
153 2
     *
154
     * @param SearchableInterface[] $searchableModels
155 2
     *
156
     * @return array
157 2
     */
158
    public function pushMultipleToIndices(array $searchableModels)
159 2
    {
160 2
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
161
        $indices = $this->initIndices($searchableModels[0]);
162 2
163
        return $this->processIndices($indices, function (Index $index) use ($algoliaRecords) {
164
            return $index->addObjects($algoliaRecords);
165
        });
166
    }
167
168
    /**
169
     * Updates the models data in all indices.
170
     *
171
     * @param SearchableInterface $searchableModel
172 3
     *
173
     * @return array
174 3
     */
175 3
    public function updateInIndices(SearchableInterface $searchableModel)
176
    {
177 3
        $indices = $this->initIndices($searchableModel);
178 3
        $record = $searchableModel->getAlgoliaRecord();
179 3
        $record['objectID'] = $searchableModel->getObjectID();
180 3
181 3
        return $this->processIndices($indices, function (Index $index) use ($record) {
182
            return $index->saveObject($record);
183 3
        });
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
        return $this->processIndices($indices, function (Index $index) use ($algoliaRecords) {
199
            return $index->saveObjects($algoliaRecords);
200 2
        });
201
    }
202 2
203 2
    /**
204
     * Removes a searchable model from indices.
205 2
     *
206
     * @param SearchableInterface $searchableModel
207
     *
208
     * @return array
209
     * @throws \InvalidArgumentException
210
     */
211
    public function removeFromIndices(SearchableInterface $searchableModel)
212
    {
213
        $indices = $indices = $this->initIndices($searchableModel);
214
        $objectID = $searchableModel->getObjectID();
215
216 3
        return $this->processIndices($indices, function (Index $index) use ($objectID) {
217
            return $index->deleteObject($objectID);
218 3
        });
219 3
    }
220
221 3
    /**
222 3
     * Removes multiple models from all indices. The given searchable models must be of the same class.
223 3
     *
224 3
     * @param array $searchableModels
225
     *
226 3
     * @return array
227
     * @throws \InvalidArgumentException
228
     */
229
    public function removeMultipleFromIndices(array $searchableModels)
230
    {
231
        $algoliaRecords = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
232
        $indices = $this->initIndices($searchableModels[0]);
233
        $objectIds = \array_map(function ($algoliaRecord) {
234
            return $algoliaRecord['objectID'];
235
        }, $algoliaRecords);
236
237 5
        return $this->processIndices($indices, function (Index $index) use ($objectIds) {
238
            return $index->deleteObjects($objectIds);
239 5
        });
240 2
    }
241
242 2
    /**
243 2
     * Re-indexes the indices safely for the given ActiveRecord Class.
244
     *
245 2
     * @param string $className The name of the ActiveRecord to be indexed
246
     *
247 2
     * @return array
248
     */
249 2
    public function reindex($className)
250 2
    {
251
        $this->checkImplementsSearchableInterface($className);
252 2
        $activeRecord = $this->activeRecordFactory->make($className);
253
254
        $records = $this->activeQueryChunker->chunk(
255
            $activeRecord->find(),
256
            self::CHUNK_SIZE,
257
            function ($activeRecordEntities) {
258
                return $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
259
            }
260
        );
261
262 4
        /* @var SearchableInterface $activeRecord */
263
        $indices = $this->initIndices($activeRecord);
264 4
265 3
        return $this->processIndices($indices, function (Index $index) use ($records) {
266
            return $this->reindexAtomically($index, $records);
267 3
        });
268 3
    }
269 3
270
    /**
271 3
     * Re-indexes the related indices for the given array only with the objects from the given array.
272
     * The given array must consist of Searchable objects of same class.
273 3
     *
274
     * @param SearchableInterface[] $searchableModels
275
     *
276 3
     * @throws \InvalidArgumentException
277 3
     * @return array
278
     */
279 3
    public function reindexOnly(array $searchableModels)
280 3
    {
281 3
        $records = $this->getAlgoliaRecordsFromSearchableModelArray($searchableModels);
282
        $indices = $this->initIndices($searchableModels[0]);
283 3
284
        return $this->processIndices($indices, function (Index $index) use ($records) {
285
            return $this->reindexAtomically($index, $records);
286
        });
287
    }
288
289
    /**
290
     * Re-indexes the related indices for the given ActiveQueryInterface.
291
     * The result of the given ActiveQuery must consist from Searchable models of the same class.
292
     *
293
     * @param ActiveQueryInterface $activeQuery
294
     *
295 3
     * @return array
296
     */
297 3
    public function reindexByActiveQuery(ActiveQueryInterface $activeQuery)
298 1
    {
299
        $indices = null;
300 1
        $records = $this->activeQueryChunker->chunk(
301
            $activeQuery,
302 1
            self::CHUNK_SIZE,
303 1
            function ($activeRecordEntities) use (&$indices) {
304 1
                $records = $this->getAlgoliaRecordsFromSearchableModelArray($activeRecordEntities);
305
306 1
                // The converting ActiveRecords to Algolia ones already does the type checking
307
                // so it's safe to init indices here during the first chunk.
308
                if ($indices === null) {
309
                    $indices = $this->initIndices($activeRecordEntities[0]);
310
                }
311
312
                return $records;
313
            }
314
        );
315
316
        return $this->processIndices($indices, function (Index $index) use ($records) {
0 ignored issues
show
Documentation introduced by
$indices is of type null, but the function expects a array<integer,object<AlgoliaSearch\Index>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
317 5
            return $this->reindexAtomically($index, $records);
318
        });
319 5
    }
320
321 5
    /**
322 5
     * Clears the indices for the given Class that implements SearchableInterface.
323 5
     *
324
     * @param string $className The name of the Class which indices are to be cleared.
325 5
     *
326
     * @throws \InvalidArgumentException
327
     * @return array
328
     */
329 2
    public function clearIndices($className)
330 2
    {
331 2
        $this->checkImplementsSearchableInterface($className);
332
        /** @var SearchableInterface $activeRecord */
333 2
        $activeRecord = $this->activeRecordFactory->make($className);
334
        $indices = $indices = $this->initIndices($activeRecord);
335 5
336
        return $this->processIndices($indices, function (Index $index) {
337 2
            return $index->clearIndex();
338
        });
339 2
    }
340 2
341 2
    /**
342
     * @param string $className The name of the class which is to be searched.
343 2
     * @param string $query
344
     * @param null|array $searchParameters Optional search parameters given as an associative array.
345
     *
346
     * @link https://github.com/algolia/algoliasearch-client-php#search-parameters Allowed search parameters.
347
     *
348
     * @return array
349
     */
350
    public function search($className, $query, array $searchParameters = null)
351
    {
352
        $this->checkImplementsSearchableInterface($className);
353
        /* @var SearchableInterface $activeRecord */
354 2
        $activeRecord = $this->activeRecordFactory->make($className);
355
        $indices = $indices = $this->initIndices($activeRecord);
356 2
357 1
        return $this->processIndices($indices, function (Index $index) use ($query, $searchParameters) {
358 1
            return $index->search($query, $searchParameters);
359
        });
360
    }
361 1
362
    /**
363 1
     * Dynamically pass methods to the Algolia Client.
364 1
     *
365 1
     * @param string $method
366
     * @param array $parameters
367 1
     *
368
     * @return mixed
369
     */
370
    public function __call($method, $parameters)
371
    {
372
        return \call_user_func_array([$this->getClient(), $method], $parameters);
373
    }
374
375
    /**
376
     * Checks if the given class implements SearchableInterface.
377
     *
378
     * @param string $class Either name or instance of the class to be checked.
379 5
     */
380
    private function checkImplementsSearchableInterface($class)
381 5
    {
382 4
        $reflectionClass = new \ReflectionClass($class);
383 4
384
        if (! $reflectionClass->implementsInterface(SearchableInterface::class)) {
385
            throw new \InvalidArgumentException("The class: {$reflectionClass->name} doesn't implement leinonen\\Yii2Algolia\\SearchableInterface");
386 4
        }
387
    }
388 4
389 4
    /**
390 4
     * Initializes indices for the given SearchableModel.
391
     *
392 4
     * @param SearchableInterface $searchableModel
393
     *
394
     * @return Index[]
395
     */
396
    private function initIndices(SearchableInterface $searchableModel)
397
    {
398
        $indexNames = $searchableModel->getIndices();
399
400
        return \array_map(function ($indexName) {
401
            if ($this->env !== null) {
402
                $indexName = $this->env . '_' . $indexName;
403 26
            }
404
405 26
            return $this->initIndex($indexName);
406
        }, $indexNames);
407
    }
408
409
    /**
410
     * Maps an array of searchable models into an Algolia friendly array.
411
     *
412
     * @param SearchableInterface[] $searchableModels
413 29
     *
414
     * @return array
415 29
     */
416
    private function getAlgoliaRecordsFromSearchableModelArray(array $searchableModels)
417 29
    {
418 7
        if (empty($searchableModels)) {
419
            throw new \InvalidArgumentException('The given array should not be empty');
420 22
        }
421
422
        // Use the first element of the array to define what kind of models we are indexing.
423
        $arrayType = \get_class($searchableModels[0]);
424
        $this->checkImplementsSearchableInterface($arrayType);
425
426
        return \array_map(function (SearchableInterface $searchableModel) use ($arrayType) {
427
            if (! $searchableModel instanceof $arrayType) {
428
                throw new \InvalidArgumentException('The given array should not contain multiple different classes');
429 25
            }
430
431 25
            $algoliaRecord = $searchableModel->getAlgoliaRecord();
432
            $algoliaRecord['objectID'] = $searchableModel->getObjectID();
433
434 25
            return $algoliaRecord;
435 3
        }, $searchableModels);
436 3
    }
437
438 25
    /**
439 25
     * Reindex atomically the given index with the given records.
440
     *
441 25
     * @param Index $index
442
     * @param array $algoliaRecords
443
     *
444
     * @return mixed
445
     */
446
    private function reindexAtomically(Index $index, array $algoliaRecords)
447
    {
448
        $temporaryIndexName = 'tmp_' . $index->indexName;
449
450
        $temporaryIndex = $this->initIndex($temporaryIndexName);
451 25
        $temporaryIndex->addObjects($algoliaRecords);
452
453 25
        $settings = $index->getSettings();
454 4
455
        // Temporary index overrides all the settings on the main one.
456
        // So we need to set the original settings on the temporary one before atomically moving the index.
457
        $temporaryIndex->setSettings($settings);
458 21
459 21
        return $this->moveIndex($temporaryIndexName, $index->indexName);
460
    }
461 17
462 17
    /**
463 5
     * Performs actions for given indices returning an array of responses from those actions.
464
     *
465
     * @param Index[] $indices
466 17
     * @param callable $callback
467 17
     *
468
     * @return array The response as an array in format of ['indexName' => $responseFromAlgoliaClient]
469 17
     */
470 17
    private function processIndices($indices, callable $callback)
471
    {
472 12
        $response = [];
473
474
        foreach ($indices as $index) {
475
            $response[$index->indexName] = \call_user_func($callback, $index);
476
        }
477
478
        return $response;
479
    }
480
}
481