Completed
Pull Request — 1.1 (#633)
by
unknown
03:19
created

Manager   C

Complexity

Total Complexity 70

Size/Duplication

Total Lines 633
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 14
Bugs 0 Features 2
Metric Value
wmc 70
c 14
b 0
f 2
lcom 1
cbo 8
dl 0
loc 633
rs 5.4305

35 Methods

Rating   Name   Duplication   Size   Complexity  
A getRepository() 0 17 3
A setCommitMode() 0 8 4
A setBulkCommitSize() 0 4 1
A createRepository() 0 4 1
A persist() 0 7 1
A remove() 0 14 2
A __construct() 0 15 1
A getClient() 0 4 1
A getName() 0 4 1
A getConfig() 0 4 1
A getMetadataCollector() 0 4 1
A getConverter() 0 4 1
A getCommitMode() 0 4 1
A getBulkCommitSize() 0 4 1
A search() 0 13 2
A flush() 0 5 1
A refresh() 0 5 1
B commit() 0 23 4
D bulk() 0 42 10
A setBulkParams() 0 4 1
A createIndex() 0 8 2
A dropIndex() 0 4 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 4 1
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

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\Missing404Exception;
16
use ONGR\ElasticsearchBundle\Mapping\MetadataCollector;
17
use ONGR\ElasticsearchBundle\Result\AbstractResultsIterator;
18
use ONGR\ElasticsearchBundle\Result\Converter;
19
use ONGR\ElasticsearchBundle\Result\DocumentIterator;
20
use ONGR\ElasticsearchBundle\Result\RawIterator;
21
use ONGR\ElasticsearchBundle\Result\Result;
22
use ONGR\ElasticsearchDSL\Search;
23
24
/**
25
 * Manager class.
26
 */
27
class Manager
28
{
29
    /**
30
     * @var string Manager name
31
     */
32
    private $name;
33
34
    /**
35
     * @var array Manager configuration
36
     */
37
    private $config = [];
38
39
    /**
40
     * @var Client
41
     */
42
    private $client;
43
44
    /**
45
     * @var Converter
46
     */
47
    private $converter;
48
49
    /**
50
     * @var array Container for bulk queries
51
     */
52
    private $bulkQueries = [];
53
54
    /**
55
     * @var array Holder for consistency, refresh and replication parameters
56
     */
57
    private $bulkParams = [];
58
59
    /**
60
     * @var array
61
     */
62
    private $indexSettings;
63
64
    /**
65
     * @var MetadataCollector
66
     */
67
    private $metadataCollector;
68
69
    /**
70
     * After commit to make data available the refresh or flush operation is needed
71
     * so one of those methods has to be defined, the default is refresh.
72
     *
73
     * @var string
74
     */
75
    private $commitMode = 'refresh';
76
77
    /**
78
     * The size that defines after how much document inserts call commit function.
79
     *
80
     * @var int
81
     */
82
    private $bulkCommitSize = 100;
83
84
    /**
85
     * Container to count how many documents was passed to the bulk query.
86
     *
87
     * @var int
88
     */
89
    private $bulkCount = 0;
90
91
    /**
92
     * @var Repository[] Repository local cache
93
     */
94
    private $repositories;
95
96
    /**
97
     * @param string            $name              Manager name
98
     * @param array             $config            Manager configuration
99
     * @param Client            $client
100
     * @param array             $indexSettings
101
     * @param MetadataCollector $metadataCollector
102
     * @param Converter         $converter
103
     */
104
    public function __construct(
105
        $name,
106
        array $config,
107
        $client,
108
        array $indexSettings,
109
        $metadataCollector,
110
        $converter
111
    ) {
112
        $this->name = $name;
113
        $this->config = $config;
114
        $this->client = $client;
115
        $this->indexSettings = $indexSettings;
116
        $this->metadataCollector = $metadataCollector;
117
        $this->converter = $converter;
118
    }
119
120
    /**
121
     * Returns Elasticsearch connection.
122
     *
123
     * @return Client
124
     */
125
    public function getClient()
126
    {
127
        return $this->client;
128
    }
129
130
    /**
131
     * @return string
132
     */
133
    public function getName()
134
    {
135
        return $this->name;
136
    }
137
138
    /**
139
     * @return array
140
     */
141
    public function getConfig()
142
    {
143
        return $this->config;
144
    }
145
146
    /**
147
     * Returns repository by document class.
148
     *
149
     * @param string $className FQCN or string in Bundle:Document format
150
     *
151
     * @return Repository
152
     */
153
    public function getRepository($className)
154
    {
155
        if (!is_string($className)) {
156
            throw new \InvalidArgumentException('Document class must be a string.');
157
        }
158
159
        $namespace = $this->getMetadataCollector()->getClassName($className);
160
161
        if (isset($this->repositories[$namespace])) {
162
            return $this->repositories[$namespace];
163
        }
164
165
        $repository = $this->createRepository($namespace);
166
        $this->repositories[$namespace] = $repository;
167
168
        return $repository;
169
    }
170
171
    /**
172
     * @return MetadataCollector
173
     */
174
    public function getMetadataCollector()
175
    {
176
        return $this->metadataCollector;
177
    }
178
179
    /**
180
     * @return Converter
181
     */
182
    public function getConverter()
183
    {
184
        return $this->converter;
185
    }
186
187
    /**
188
     * @return string
189
     */
190
    public function getCommitMode()
191
    {
192
        return $this->commitMode;
193
    }
194
195
    /**
196
     * @param string $commitMode
197
     */
198
    public function setCommitMode($commitMode)
199
    {
200
        if ($commitMode === 'refresh' || $commitMode === 'flush' || $commitMode === 'none') {
201
            $this->commitMode = $commitMode;
202
        } else {
203
            throw new \LogicException('The commit method must be either refresh, flush or none.');
204
        }
205
    }
206
207
    /**
208
     * @return int
209
     */
210
    public function getBulkCommitSize()
211
    {
212
        return $this->bulkCommitSize;
213
    }
214
215
    /**
216
     * @param int $bulkCommitSize
217
     */
218
    public function setBulkCommitSize($bulkCommitSize)
219
    {
220
        $this->bulkCommitSize = $bulkCommitSize;
221
    }
222
223
    /**
224
     * Creates a repository.
225
     *
226
     * @param string $className
227
     *
228
     * @return Repository
229
     */
230
    private function createRepository($className)
231
    {
232
        return new Repository($this, $className);
233
    }
234
235
    /**
236
     * Executes search query in the index.
237
     *
238
     * @param array $types             List of types to search in.
239
     * @param array $query             Query to execute.
240
     * @param array $queryStringParams Query parameters.
241
     *
242
     * @return array
243
     */
244
    public function search(array $types, array $query, array $queryStringParams = [])
245
    {
246
        $params = [];
247
        $params['index'] = $this->getIndexName();
248
        $params['type'] = implode(',', $types);
249
        $params['body'] = $query;
250
251
        if (!empty($queryStringParams)) {
252
            $params = array_merge($queryStringParams, $params);
253
        }
254
255
        return $this->client->search($params);
256
    }
257
258
    /**
259
     * Adds document to next flush.
260
     *
261
     * @param object $document
262
     */
263
    public function persist($document)
264
    {
265
        $documentArray = $this->converter->convertToArray($document);
266
        $type = $this->getMetadataCollector()->getDocumentType(get_class($document));
267
268
        $this->bulk('index', $type, $documentArray);
269
    }
270
271
    /**
272
     * Adds document for removal.
273
     *
274
     * @param object $document
275
     */
276
    public function remove($document)
277
    {
278
        $data = $this->converter->convertToArray($document, [], ['_id']);
279
280
        if (!isset($data['_id'])) {
281
            throw new \LogicException(
282
                'In order to use remove() method document class must have property with @Id annotation.'
283
            );
284
        }
285
286
        $type = $this->getMetadataCollector()->getDocumentType(get_class($document));
287
288
        $this->bulk('delete', $type, ['_id' => $data['_id']]);
289
    }
290
291
    /**
292
     * Flushes elasticsearch index.
293
     *
294
     * @param array $params
295
     *
296
     * @return array
297
     */
298
    public function flush(array $params = [])
299
    {
300
        $params = array_merge($params, ['index' => $this->getIndexName()]);
301
        return $this->client->indices()->flush($params);
302
    }
303
304
    /**
305
     * Refreshes elasticsearch index.
306
     *
307
     * @param array $params
308
     *
309
     * @return array
310
     */
311
    public function refresh(array $params = [])
312
    {
313
        $params = array_merge($params, ['index' => $this->getIndexName()]);
314
        return $this->client->indices()->refresh($params);
315
    }
316
317
    /**
318
     * Inserts the current query container to the index, used for bulk queries execution.
319
     *
320
     * @param array $params Parameters that will be passed to the flush or refresh queries.
321
     *
322
     * @return null|array
323
     */
324
    public function commit(array $params = [])
325
    {
326
        if (!empty($this->bulkQueries)) {
327
            $bulkQueries = array_merge($this->bulkQueries, $this->bulkParams);
328
329
            $bulkResponse = $this->client->bulk($bulkQueries);
330
            $this->bulkQueries = [];
331
            $this->bulkCount = 0;
332
333
            switch ($this->getCommitMode()) {
334
                case 'flush':
335
                    $this->flush($params);
336
                    break;
337
                case 'refresh':
338
                    $this->refresh($params);
339
                    break;
340
            }
341
342
            return $bulkResponse;
343
        }
344
345
        return null;
346
    }
347
348
    /**
349
     * Adds query to bulk queries container.
350
     *
351
     * @param string       $operation One of: index, update, delete, create.
352
     * @param string|array $type      Elasticsearch type name.
353
     * @param array        $query     DSL to execute.
354
     *
355
     * @throws \InvalidArgumentException
356
     *
357
     * @return null|array
358
     */
359
    public function bulk($operation, $type, array $query)
360
    {
361
        if (!in_array($operation, ['index', 'create', 'update', 'delete'])) {
362
            throw new \InvalidArgumentException('Wrong bulk operation selected');
363
        }
364
365
        $this->bulkQueries['body'][] = [
366
            $operation => array_filter(
367
                [
368
                    '_index' => $this->getIndexName(),
369
                    '_type' => $type,
370
                    '_id' => isset($query['_id']) ? $query['_id'] : null,
371
                    '_ttl' => isset($query['_ttl']) ? $query['_ttl'] : null,
372
                    '_parent' => isset($query['_parent']) ? $query['_parent'] : null,
373
                ]
374
            ),
375
        ];
376
        unset($query['_id'], $query['_ttl'], $query['_parent']);
377
378
        switch ($operation) {
379
            case 'index':
380
            case 'create':
381
            case 'update':
382
                $this->bulkQueries['body'][] = $query;
383
                break;
384
            case 'delete':
385
                // Body for delete operation is not needed to apply.
386
            default:
387
                // Do nothing.
388
                break;
389
        }
390
391
        // We are using counter because there is to difficult to resolve this from bulkQueries array.
392
        $this->bulkCount++;
393
394
        $response = null;
395
        if ($this->bulkCommitSize === $this->bulkCount) {
396
            $response = $this->commit();
397
        }
398
399
        return $response;
400
    }
401
402
    /**
403
     * Optional setter to change bulk query params.
404
     *
405
     * @param array $params Possible keys:
406
     *                      ['consistency'] = (enum) Explicit write consistency setting for the operation.
407
     *                      ['refresh']     = (boolean) Refresh the index after performing the operation.
408
     *                      ['replication'] = (enum) Explicitly set the replication type.
409
     */
410
    public function setBulkParams(array $params)
411
    {
412
        $this->bulkParams = $params;
413
    }
414
415
    /**
416
     * Creates fresh elasticsearch index.
417
     *
418
     * @param bool $noMapping Determines if mapping should be included.
419
     *
420
     * @return array
421
     */
422
    public function createIndex($noMapping = false)
423
    {
424
        if ($noMapping) {
425
            unset($this->indexSettings['body']['mappings']);
426
        }
427
428
        return $this->getClient()->indices()->create($this->indexSettings);
429
    }
430
431
    /**
432
     * Drops elasticsearch index.
433
     */
434
    public function dropIndex()
435
    {
436
        return $this->getClient()->indices()->delete(['index' => $this->getIndexName()]);
437
    }
438
439
    /**
440
     * Tries to drop and create fresh elasticsearch index.
441
     *
442
     * @param bool $noMapping Determines if mapping should be included.
443
     *
444
     * @return array
445
     */
446
    public function dropAndCreateIndex($noMapping = false)
447
    {
448
        try {
449
            $this->dropIndex();
450
        } catch (\Exception $e) {
451
            // Do nothing, our target is to create new index.
452
        }
453
454
        return $this->createIndex($noMapping);
455
    }
456
457
    /**
458
     * Checks if connection index is already created.
459
     *
460
     * @return bool
461
     */
462
    public function indexExists()
463
    {
464
        return $this->getClient()->indices()->exists(['index' => $this->getIndexName()]);
465
    }
466
467
    /**
468
     * Returns index name this connection is attached to.
469
     *
470
     * @return string
471
     */
472
    public function getIndexName()
473
    {
474
        return $this->indexSettings['index'];
475
    }
476
477
    /**
478
     * Sets index name for this connection.
479
     *
480
     * @param string $name
481
     */
482
    public function setIndexName($name)
483
    {
484
        $this->indexSettings['index'] = $name;
485
    }
486
487
    /**
488
     * Returns Elasticsearch version number.
489
     *
490
     * @return string
491
     */
492
    public function getVersionNumber()
493
    {
494
        return $this->client->info()['version']['number'];
495
    }
496
497
    /**
498
     * Clears elasticsearch client cache.
499
     */
500
    public function clearCache()
501
    {
502
        $this->getClient()->indices()->clearCache(['index' => $this->getIndexName()]);
503
    }
504
505
    /**
506
     * Returns a single document by ID. Returns NULL if document was not found.
507
     *
508
     * @param string $className Document class name or Elasticsearch type name
509
     * @param string $id        Document ID to find
510
     *
511
     * @return object
512
     */
513
    public function find($className, $id)
514
    {
515
        $type = $this->resolveTypeName($className);
516
517
        $params = [
518
            'index' => $this->getIndexName(),
519
            'type' => $type,
520
            'id' => $id,
521
        ];
522
523
        try {
524
            $result = $this->getClient()->get($params);
525
        } catch (Missing404Exception $e) {
526
            return null;
527
        }
528
529
        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...
530
    }
531
532
    /**
533
     * Executes given search.
534
     *
535
     * @param array  $types
536
     * @param Search $search
537
     * @param string $resultsType
538
     *
539
     * @return DocumentIterator|RawIterator|array
540
     */
541
    public function execute($types, Search $search, $resultsType = Result::RESULTS_OBJECT)
542
    {
543
        foreach ($types as &$type) {
544
            $type = $this->resolveTypeName($type);
545
        }
546
547
        $results = $this->search($types, $search->toArray(), $search->getQueryParams());
548
549
        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...
550
    }
551
552
    /**
553
     * Parses raw result.
554
     *
555
     * @param array  $raw
556
     * @param string $resultsType
557
     * @param string $scrollDuration
558
     *
559
     * @return DocumentIterator|RawIterator|array
560
     *
561
     * @throws \Exception
562
     */
563
    private function parseResult($raw, $resultsType, $scrollDuration = null)
564
    {
565
        $scrollConfig = [];
566
        if (isset($raw['_scroll_id'])) {
567
            $scrollConfig['_scroll_id'] = $raw['_scroll_id'];
568
            $scrollConfig['duration'] = $scrollDuration;
569
        }
570
571
        switch ($resultsType) {
572
            case Result::RESULTS_OBJECT:
573
                return new DocumentIterator($raw, $this, $scrollConfig);
574
            case Result::RESULTS_ARRAY:
575
                return $this->convertToNormalizedArray($raw);
576
            case Result::RESULTS_RAW:
577
                return $raw;
578
            case Result::RESULTS_RAW_ITERATOR:
579
                return new RawIterator($raw, $this, $scrollConfig);
580
            default:
581
                throw new \Exception('Wrong results type selected');
582
        }
583
    }
584
585
    /**
586
     * Normalizes response array.
587
     *
588
     * @param array $data
589
     *
590
     * @return array
591
     */
592
    private function convertToNormalizedArray($data)
593
    {
594
        if (array_key_exists('_source', $data)) {
595
            return $data['_source'];
596
        }
597
598
        $output = [];
599
600
        if (isset($data['hits']['hits'][0]['_source'])) {
601
            foreach ($data['hits']['hits'] as $item) {
602
                $output[] = $item['_source'];
603
            }
604
        } elseif (isset($data['hits']['hits'][0]['fields'])) {
605
            foreach ($data['hits']['hits'] as $item) {
606
                $output[] = array_map('reset', $item['fields']);
607
            }
608
        }
609
610
        return $output;
611
    }
612
613
    /**
614
     * Fetches next set of results.
615
     *
616
     * @param string $scrollId
617
     * @param string $scrollDuration
618
     * @param string $resultsType
619
     *
620
     * @return AbstractResultsIterator
621
     *
622
     * @throws \Exception
623
     */
624
    public function scroll(
625
        $scrollId,
626
        $scrollDuration = '5m',
627
        $resultsType = Result::RESULTS_OBJECT
628
    ) {
629
        $results = $this->getClient()->scroll(['scroll_id' => $scrollId, 'scroll' => $scrollDuration]);
630
631
        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...
632
    }
633
634
    /**
635
     * Clears scroll.
636
     *
637
     * @param string $scrollId
638
     */
639
    public function clearScroll($scrollId)
640
    {
641
        $this->getClient()->clearScroll(['scroll_id' => $scrollId]);
642
    }
643
644
    /**
645
     * Resolves type name by class name.
646
     *
647
     * @param string $className
648
     *
649
     * @return string
650
     */
651
    private function resolveTypeName($className)
652
    {
653
        if (strpos($className, ':') !== false || strpos($className, '\\') !== false) {
654
            return $this->getMetadataCollector()->getDocumentType($className);
655
        }
656
657
        return $className;
658
    }
659
}
660