Completed
Pull Request — master (#589)
by
unknown
02:25
created

Manager   D

Complexity

Total Complexity 85

Size/Duplication

Total Lines 723
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 13
Bugs 0 Features 3
Metric Value
wmc 85
c 13
b 0
f 3
lcom 1
cbo 9
dl 0
loc 723
rs 4.4444

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
A getClient() 0 4 1
A getName() 0 4 1
A getConfig() 0 4 1
A getRepository() 0 17 3
A getMetadataCollector() 0 4 1
A getConverter() 0 4 1
A getCommitMode() 0 4 1
A setCommitMode() 0 8 4
A getBulkCommitSize() 0 4 1
A setBulkCommitSize() 0 4 1
A createRepository() 0 4 1
A search() 0 13 2
A persist() 0 10 2
A remove() 0 14 2
A flush() 0 4 1
A refresh() 0 4 1
B commit() 0 25 4
D bulk() 0 42 10
A setBulkParams() 0 4 1
A createIndex() 0 10 2
A dropIndex() 0 6 1
A dropAndCreateIndex() 0 10 2
A indexExists() 0 4 1
A getIndexName() 0 4 1
A setIndexName() 0 4 1
A getVersionNumber() 0 4 1
A clearCache() 0 6 1
A setReadOnly() 0 4 1
A isReadOnly() 0 6 2
A find() 0 18 2
A execute() 0 10 2
B parseResult() 0 21 6
B convertToNormalizedArray() 0 20 6
A scroll() 0 9 1
A clearScroll() 0 4 1
A resolveTypeName() 0 8 3
C addIds() 0 36 11

How to fix   Complexity   

Complex Class

Complex classes like Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the ONGR package.
5
 *
6
 * (c) NFQ Technologies UAB <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ONGR\ElasticsearchBundle\Service;
13
14
use Elasticsearch\Client;
15
use Elasticsearch\Common\Exceptions\Forbidden403Exception;
16
use Elasticsearch\Common\Exceptions\Missing404Exception;
17
use ONGR\ElasticsearchBundle\Mapping\MetadataCollector;
18
use ONGR\ElasticsearchBundle\Result\AbstractResultsIterator;
19
use ONGR\ElasticsearchBundle\Result\Converter;
20
use ONGR\ElasticsearchBundle\Result\DocumentIterator;
21
use ONGR\ElasticsearchBundle\Result\RawIterator;
22
use ONGR\ElasticsearchBundle\Result\Result;
23
use ONGR\ElasticsearchDSL\Search;
24
25
/**
26
 * Manager class.
27
 */
28
class Manager
29
{
30
    /**
31
     * @var string Manager name
32
     */
33
    private $name;
34
35
    /**
36
     * @var array Manager configuration
37
     */
38
    private $config = [];
39
40
    /**
41
     * @var Client
42
     */
43
    private $client;
44
45
    /**
46
     * @var Converter
47
     */
48
    private $converter;
49
50
    /**
51
     * @var bool
52
     */
53
    private $readOnly;
54
55
    /**
56
     * @var array Container for bulk queries
57
     */
58
    private $bulkQueries = [];
59
60
    /**
61
     * @var array Holder for consistency, refresh and replication parameters
62
     */
63
    private $bulkParams = [];
64
65
    /**
66
     * @var array
67
     */
68
    private $indexSettings;
69
70
    /**
71
     * @var MetadataCollector
72
     */
73
    private $metadataCollector;
74
75
    /**
76
     * After commit to make data available the refresh or flush operation is needed
77
     * so one of those methods has to be defined, the default is refresh.
78
     *
79
     * @var string
80
     */
81
    private $commitMode = 'refresh';
82
83
    /**
84
     * Reference to the persisted objects that need to be given an ID that returns from
85
     * elasticsearch
86
     *
87
     * @var array
88
     */
89
    private $persistedObjects = [];
90
91
    /**
92
     * The size that defines after how much document inserts call commit function.
93
     *
94
     * @var int
95
     */
96
    private $bulkCommitSize = 100;
97
98
    /**
99
     * Container to count how many documents was passed to the bulk query.
100
     *
101
     * @var int
102
     */
103
    private $bulkCount = 0;
104
105
    /**
106
     * @var Repository[] Repository local cache
107
     */
108
    private $repositories;
109
110
    /**
111
     * @param string            $name              Manager name
112
     * @param array             $config            Manager configuration
113
     * @param Client            $client
114
     * @param array             $indexSettings
115
     * @param MetadataCollector $metadataCollector
116
     * @param Converter         $converter
117
     */
118
    public function __construct(
119
        $name,
120
        array $config,
121
        $client,
122
        array $indexSettings,
123
        $metadataCollector,
124
        $converter
125
    ) {
126
        $this->name = $name;
127
        $this->config = $config;
128
        $this->client = $client;
129
        $this->indexSettings = $indexSettings;
130
        $this->metadataCollector = $metadataCollector;
131
        $this->converter = $converter;
132
133
        $this->setReadOnly($config['readonly']);
134
    }
135
136
    /**
137
     * Returns Elasticsearch connection.
138
     *
139
     * @return Client
140
     */
141
    public function getClient()
142
    {
143
        return $this->client;
144
    }
145
146
    /**
147
     * @return string
148
     */
149
    public function getName()
150
    {
151
        return $this->name;
152
    }
153
154
    /**
155
     * @return array
156
     */
157
    public function getConfig()
158
    {
159
        return $this->config;
160
    }
161
162
    /**
163
     * Returns repository by document class.
164
     *
165
     * @param string $className FQCN or string in Bundle:Document format
166
     *
167
     * @return Repository
168
     */
169
    public function getRepository($className)
170
    {
171
        if (!is_string($className)) {
172
            throw new \InvalidArgumentException('Document class must be a string.');
173
        }
174
175
        $namespace = $this->getMetadataCollector()->getClassName($className);
176
177
        if (isset($this->repositories[$namespace])) {
178
            return $this->repositories[$namespace];
179
        }
180
181
        $repository = $this->createRepository($namespace);
182
        $this->repositories[$namespace] = $repository;
183
184
        return $repository;
185
    }
186
187
    /**
188
     * @return MetadataCollector
189
     */
190
    public function getMetadataCollector()
191
    {
192
        return $this->metadataCollector;
193
    }
194
195
    /**
196
     * @return Converter
197
     */
198
    public function getConverter()
199
    {
200
        return $this->converter;
201
    }
202
203
    /**
204
     * @return string
205
     */
206
    public function getCommitMode()
207
    {
208
        return $this->commitMode;
209
    }
210
211
    /**
212
     * @param string $commitMode
213
     */
214
    public function setCommitMode($commitMode)
215
    {
216
        if ($commitMode === 'refresh' || $commitMode === 'flush' || $commitMode === 'none') {
217
            $this->commitMode = $commitMode;
218
        } else {
219
            throw new \LogicException('The commit method must be either refresh, flush or none.');
220
        }
221
    }
222
223
    /**
224
     * @return int
225
     */
226
    public function getBulkCommitSize()
227
    {
228
        return $this->bulkCommitSize;
229
    }
230
231
    /**
232
     * @param int $bulkCommitSize
233
     */
234
    public function setBulkCommitSize($bulkCommitSize)
235
    {
236
        $this->bulkCommitSize = $bulkCommitSize;
237
    }
238
239
    /**
240
     * Creates a repository.
241
     *
242
     * @param string $className
243
     *
244
     * @return Repository
245
     */
246
    private function createRepository($className)
247
    {
248
        return new Repository($this, $className);
249
    }
250
251
    /**
252
     * Executes search query in the index.
253
     *
254
     * @param array $types             List of types to search in.
255
     * @param array $query             Query to execute.
256
     * @param array $queryStringParams Query parameters.
257
     *
258
     * @return array
259
     */
260
    public function search(array $types, array $query, array $queryStringParams = [])
261
    {
262
        $params = [];
263
        $params['index'] = $this->getIndexName();
264
        $params['type'] = implode(',', $types);
265
        $params['body'] = $query;
266
267
        if (!empty($queryStringParams)) {
268
            $params = array_merge($queryStringParams, $params);
269
        }
270
271
        return $this->client->search($params);
272
    }
273
274
    /**
275
     * Adds document to next flush.
276
     *
277
     * @param object $document
278
     */
279
    public function persist($document)
280
    {
281
        $documentArray = $this->converter->convertToArray($document);
282
        $type = $this->getMetadataCollector()->getDocumentType(get_class($document));
283
284
        $this->bulk('index', $type, $documentArray);
285
        if (!isset($document->id)) {
286
            $this->persistedObjects[] = $document;
287
        }
288
    }
289
290
    /**
291
     * Adds document for removal.
292
     *
293
     * @param object $document
294
     */
295
    public function remove($document)
296
    {
297
        $data = $this->converter->convertToArray($document, [], ['_id']);
298
299
        if (!isset($data['_id'])) {
300
            throw new \LogicException(
301
                'In order to use remove() method document class must have property with @Id annotation.'
302
            );
303
        }
304
305
        $type = $this->getMetadataCollector()->getDocumentType(get_class($document));
306
307
        $this->bulk('delete', $type, ['_id' => $data['_id']]);
308
    }
309
310
    /**
311
     * Flushes elasticsearch index.
312
     *
313
     * @param array $params
314
     *
315
     * @return array
316
     */
317
    public function flush(array $params = [])
318
    {
319
        return $this->client->indices()->flush($params);
320
    }
321
322
    /**
323
     * Refreshes elasticsearch index.
324
     *
325
     * @param array $params
326
     *
327
     * @return array
328
     */
329
    public function refresh(array $params = [])
330
    {
331
        return $this->client->indices()->refresh($params);
332
    }
333
334
    /**
335
     * Adds ids to documents
336
     *
337
     * @param array $bulkQueries
338
     *
339
     * @param array $bulkResponse
340
     */
341
    public function addIds(array $bulkQueries, $bulkResponse = [])
342
    {
343
        if (empty($bulkResponse)) {
344
            $this->persistedObjects = [];
345
            return;
346
        }
347
        $indexing = [];
348
        foreach ($bulkQueries['body'] as $number => $query) {
349
            if (isset($query['index']) && !isset($query['index']['_id'])) {
350
                $indexing[] = $number / 2;
351
            }
352
        }
353
        if (isset($bulkQueries['body'][0]['index'])) {
354
            if (isset($this->persistedObjects)) {
355
                $i = 0;
356
                foreach ($this->persistedObjects as $document) {
357
                    $class = get_class($document);
358
                    $mapping = $this->metadataCollector->getMapping($class);
359
360
                    if (isset($mapping['aliases']['_id'])) {
361
                        $mapping = $mapping['aliases']['_id'];
362
                        $id_property = $mapping['propertyName'];
363
364
                        if ($mapping['propertyType'] == 'public') {
365
                            $document->$id_property = $bulkResponse['items'][$indexing[$i]]['create']['_id'];
366
                        } elseif (isset($mapping['methods']['setter'])) {
367
                            $method = $mapping['methods']['setter'];
368
                            $document->$method($bulkResponse['items'][$indexing[$i]]['create']['_id']);
369
                        }
370
                    }
371
                    $i++;
372
                }
373
            }
374
            $this->persistedObjects = [];
375
        }
376
    }
377
378
    /**
379
     * Inserts the current query container to the index, used for bulk queries execution.
380
     *
381
     * @param array $params Parameters that will be passed to the flush or refresh queries.
382
     *
383
     * @return null|array
384
     */
385
    public function commit(array $params = [])
386
    {
387
        $this->isReadOnly('Commit');
388
389
        if (!empty($this->bulkQueries)) {
390
            $bulkQueries = array_merge($this->bulkQueries, $this->bulkParams);
391
            $this->bulkQueries = [];
392
393
            $bulkResponse = $this->client->bulk($bulkQueries);
394
395
            switch ($this->getCommitMode()) {
396
                case 'flush':
397
                    $this->flush($params);
398
                    break;
399
                case 'refresh':
400
                    $this->refresh($params);
401
                    break;
402
            }
403
404
            $this->addIds($bulkQueries, $bulkResponse);
0 ignored issues
show
Documentation introduced by
$bulkResponse is of type callable, but the function expects a array.

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...
405
406
            return $bulkResponse;
407
        }
408
        return null;
409
    }
410
411
    /**
412
     * Adds query to bulk queries container.
413
     *
414
     * @param string       $operation One of: index, update, delete, create.
415
     * @param string|array $type      Elasticsearch type name.
416
     * @param array        $query     DSL to execute.
417
     *
418
     * @throws \InvalidArgumentException
419
     */
420
    public function bulk($operation, $type, array $query)
421
    {
422
        $this->isReadOnly('Bulk');
423
424
        if (!in_array($operation, ['index', 'create', 'update', 'delete'])) {
425
            throw new \InvalidArgumentException('Wrong bulk operation selected');
426
        }
427
428
        $this->bulkQueries['body'][] = [
429
            $operation => array_filter(
430
                [
431
                    '_index' => $this->getIndexName(),
432
                    '_type' => $type,
433
                    '_id' => isset($query['_id']) ? $query['_id'] : null,
434
                    '_ttl' => isset($query['_ttl']) ? $query['_ttl'] : null,
435
                    '_parent' => isset($query['_parent']) ? $query['_parent'] : null,
436
                ]
437
            ),
438
        ];
439
        unset($query['_id'], $query['_ttl'], $query['_parent']);
440
441
        switch ($operation) {
442
            case 'index':
443
            case 'create':
444
            case 'update':
445
                $this->bulkQueries['body'][] = $query;
446
                break;
447
            case 'delete':
448
                // Body for delete operation is not needed to apply.
449
            default:
450
                // Do nothing.
451
                break;
452
        }
453
454
        // We are using counter because there is to difficult to resolve this from bulkQueries array.
455
        $this->bulkCount++;
456
457
        if ($this->bulkCommitSize === $this->bulkCount) {
458
            $this->commit();
459
            $this->bulkCount = 0;
460
        }
461
    }
462
463
    /**
464
     * Optional setter to change bulk query params.
465
     *
466
     * @param array $params Possible keys:
467
     *                      ['consistency'] = (enum) Explicit write consistency setting for the operation.
468
     *                      ['refresh']     = (boolean) Refresh the index after performing the operation.
469
     *                      ['replication'] = (enum) Explicitly set the replication type.
470
     */
471
    public function setBulkParams(array $params)
472
    {
473
        $this->bulkParams = $params;
474
    }
475
476
    /**
477
     * Creates fresh elasticsearch index.
478
     *
479
     * @param bool $noMapping Determines if mapping should be included.
480
     *
481
     * @return array
482
     */
483
    public function createIndex($noMapping = false)
484
    {
485
        $this->isReadOnly('Create index');
486
487
        if ($noMapping) {
488
            unset($this->indexSettings['body']['mappings']);
489
        }
490
491
        return $this->getClient()->indices()->create($this->indexSettings);
492
    }
493
494
    /**
495
     * Drops elasticsearch index.
496
     */
497
    public function dropIndex()
498
    {
499
        $this->isReadOnly('Drop index');
500
501
        return $this->getClient()->indices()->delete(['index' => $this->getIndexName()]);
502
    }
503
504
    /**
505
     * Tries to drop and create fresh elasticsearch index.
506
     *
507
     * @param bool $noMapping Determines if mapping should be included.
508
     *
509
     * @return array
510
     */
511
    public function dropAndCreateIndex($noMapping = false)
512
    {
513
        try {
514
            $this->dropIndex();
515
        } catch (\Exception $e) {
516
            // Do nothing, our target is to create new index.
517
        }
518
519
        return $this->createIndex($noMapping);
520
    }
521
522
    /**
523
     * Checks if connection index is already created.
524
     *
525
     * @return bool
526
     */
527
    public function indexExists()
528
    {
529
        return $this->getClient()->indices()->exists(['index' => $this->getIndexName()]);
530
    }
531
532
    /**
533
     * Returns index name this connection is attached to.
534
     *
535
     * @return string
536
     */
537
    public function getIndexName()
538
    {
539
        return $this->indexSettings['index'];
540
    }
541
542
    /**
543
     * Sets index name for this connection.
544
     *
545
     * @param string $name
546
     */
547
    public function setIndexName($name)
548
    {
549
        $this->indexSettings['index'] = $name;
550
    }
551
552
    /**
553
     * Returns Elasticsearch version number.
554
     *
555
     * @return string
556
     */
557
    public function getVersionNumber()
558
    {
559
        return $this->client->info()['version']['number'];
560
    }
561
562
    /**
563
     * Clears elasticsearch client cache.
564
     */
565
    public function clearCache()
566
    {
567
        $this->isReadOnly('Clear cache');
568
569
        $this->getClient()->indices()->clearCache(['index' => $this->getIndexName()]);
570
    }
571
572
    /**
573
     * Set connection to read only state.
574
     *
575
     * @param bool $readOnly
576
     */
577
    public function setReadOnly($readOnly)
578
    {
579
        $this->readOnly = $readOnly;
580
    }
581
582
    /**
583
     * Checks if connection is read only.
584
     *
585
     * @param string $message Error message.
586
     *
587
     * @throws Forbidden403Exception
588
     */
589
    public function isReadOnly($message = '')
590
    {
591
        if ($this->readOnly) {
592
            throw new Forbidden403Exception("Manager is readonly! {$message} operation is not permitted.");
593
        }
594
    }
595
596
    /**
597
     * Returns a single document by ID. Returns NULL if document was not found.
598
     *
599
     * @param string $className Document class name or Elasticsearch type name
600
     * @param string $id        Document ID to find
601
     *
602
     * @return object
603
     */
604
    public function find($className, $id)
605
    {
606
        $type = $this->resolveTypeName($className);
607
608
        $params = [
609
            'index' => $this->getIndexName(),
610
            'type' => $type,
611
            'id' => $id,
612
        ];
613
614
        try {
615
            $result = $this->getClient()->get($params);
616
        } catch (Missing404Exception $e) {
617
            return null;
618
        }
619
620
        return $this->getConverter()->convertToDocument($result, $this);
0 ignored issues
show
Documentation introduced by
$result is of type callable, but the function expects a array.

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...
621
    }
622
623
    /**
624
     * Executes given search.
625
     *
626
     * @param array  $types
627
     * @param Search $search
628
     * @param string $resultsType
629
     *
630
     * @return DocumentIterator|RawIterator|array
631
     */
632
    public function execute($types, Search $search, $resultsType = Result::RESULTS_OBJECT)
633
    {
634
        foreach ($types as &$type) {
635
            $type = $this->resolveTypeName($type);
636
        }
637
638
        $results = $this->search($types, $search->toArray(), $search->getQueryParams());
639
640
        return $this->parseResult($results, $resultsType, $search->getScroll());
0 ignored issues
show
Documentation introduced by
$results is of type callable, but the function expects a array.

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...
641
    }
642
643
    /**
644
     * Parses raw result.
645
     *
646
     * @param array  $raw
647
     * @param string $resultsType
648
     * @param string $scrollDuration
649
     *
650
     * @return DocumentIterator|RawIterator|array
651
     *
652
     * @throws \Exception
653
     */
654
    private function parseResult($raw, $resultsType, $scrollDuration = null)
655
    {
656
        $scrollConfig = [];
657
        if (isset($raw['_scroll_id'])) {
658
            $scrollConfig['_scroll_id'] = $raw['_scroll_id'];
659
            $scrollConfig['duration'] = $scrollDuration;
660
        }
661
662
        switch ($resultsType) {
663
            case Result::RESULTS_OBJECT:
664
                return new DocumentIterator($raw, $this, $scrollConfig);
665
            case Result::RESULTS_ARRAY:
666
                return $this->convertToNormalizedArray($raw);
667
            case Result::RESULTS_RAW:
668
                return $raw;
669
            case Result::RESULTS_RAW_ITERATOR:
670
                return new RawIterator($raw, $this, $scrollConfig);
671
            default:
672
                throw new \Exception('Wrong results type selected');
673
        }
674
    }
675
676
    /**
677
     * Normalizes response array.
678
     *
679
     * @param array $data
680
     *
681
     * @return array
682
     */
683
    private function convertToNormalizedArray($data)
684
    {
685
        if (array_key_exists('_source', $data)) {
686
            return $data['_source'];
687
        }
688
689
        $output = [];
690
691
        if (isset($data['hits']['hits'][0]['_source'])) {
692
            foreach ($data['hits']['hits'] as $item) {
693
                $output[] = $item['_source'];
694
            }
695
        } elseif (isset($data['hits']['hits'][0]['fields'])) {
696
            foreach ($data['hits']['hits'] as $item) {
697
                $output[] = array_map('reset', $item['fields']);
698
            }
699
        }
700
701
        return $output;
702
    }
703
704
    /**
705
     * Fetches next set of results.
706
     *
707
     * @param string $scrollId
708
     * @param string $scrollDuration
709
     * @param string $resultsType
710
     *
711
     * @return AbstractResultsIterator
712
     *
713
     * @throws \Exception
714
     */
715
    public function scroll(
716
        $scrollId,
717
        $scrollDuration = '5m',
718
        $resultsType = Result::RESULTS_OBJECT
719
    ) {
720
        $results = $this->getClient()->scroll(['scroll_id' => $scrollId, 'scroll' => $scrollDuration]);
721
722
        return $this->parseResult($results, $resultsType, $scrollDuration);
0 ignored issues
show
Documentation introduced by
$results is of type callable, but the function expects a array.

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...
723
    }
724
725
    /**
726
     * Clears scroll.
727
     *
728
     * @param string $scrollId
729
     */
730
    public function clearScroll($scrollId)
731
    {
732
        $this->getClient()->clearScroll(['scroll_id' => $scrollId]);
733
    }
734
735
    /**
736
     * Resolves type name by class name.
737
     *
738
     * @param string $className
739
     *
740
     * @return string
741
     */
742
    private function resolveTypeName($className)
743
    {
744
        if (strpos($className, ':') !== false || strpos($className, '\\') !== false) {
745
            return $this->getMetadataCollector()->getDocumentType($className);
746
        }
747
748
        return $className;
749
    }
750
}
751