Completed
Pull Request — 5.0 (#747)
by Simonas
20:41
created

DocumentParser   C

Complexity

Total Complexity 79

Size/Duplication

Total Lines 559
Duplicated Lines 7.51 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 11
dl 42
loc 559
rs 5.1632
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B parse() 0 32 3
A getDocumentAnnotationData() 0 4 1
A getPropertyAnnotationData() 10 10 3
A getEmbeddedAnnotationData() 10 10 3
A getHashMapAnnotationData() 10 10 3
B getMetaFieldAnnotationData() 0 23 6
A getObjects() 0 4 1
F getAliases() 0 86 17
B getMutatorMethods() 12 44 5
A registerAnnotations() 0 19 2
A getDocumentType() 0 8 2
B getDocumentPropertiesReflection() 0 26 5
C getAnalyzers() 0 36 11
C getProperties() 0 43 12
B getObjectMapping() 0 33 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocumentParser 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 DocumentParser, 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\Mapping;
13
14
use Doctrine\Common\Annotations\AnnotationRegistry;
15
use Doctrine\Common\Annotations\Reader;
16
use ONGR\ElasticsearchBundle\Annotation\Document;
17
use ONGR\ElasticsearchBundle\Annotation\Embedded;
18
use ONGR\ElasticsearchBundle\Annotation\HashMap;
19
use ONGR\ElasticsearchBundle\Annotation\MetaField;
20
use ONGR\ElasticsearchBundle\Annotation\Nested;
21
use ONGR\ElasticsearchBundle\Annotation\ParentDocument;
22
use ONGR\ElasticsearchBundle\Annotation\Property;
23
use ONGR\ElasticsearchBundle\Exception\MissingDocumentAnnotationException;
24
25
/**
26
 * Document parser used for reading document annotations.
27
 */
28
class DocumentParser
29
{
30
    const PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Property';
31
    const EMBEDDED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Embedded';
32
    const DOCUMENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Document';
33
    const OBJECT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Object';
34
    const NESTED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Nested';
35
36
    // Meta fields
37
    const ID_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Id';
38
    const PARENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\ParentDocument';
39
    const ROUTING_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Routing';
40
    const VERSION_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Version';
41
    const HASH_MAP_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\HashMap';
42
43
    /**
44
     * @var Reader Used to read document annotations.
45
     */
46
    private $reader;
47
48
    /**
49
     * @var DocumentFinder Used to find documents.
50
     */
51
    private $finder;
52
53
    /**
54
     * @var array Contains gathered objects which later adds to documents.
55
     */
56
    private $objects = [];
57
58
    /**
59
     * @var array Document properties aliases.
60
     */
61
    private $aliases = [];
62
63
    /**
64
     * @var array Local cache for document properties.
65
     */
66
    private $properties = [];
67
68
    /**
69
     * @param Reader         $reader Used for reading annotations.
70
     * @param DocumentFinder $finder Used for resolving namespaces.
71
     */
72
    public function __construct(Reader $reader, DocumentFinder $finder)
73
    {
74
        $this->reader = $reader;
75
        $this->finder = $finder;
76
        $this->registerAnnotations();
77
    }
78
79
    /**
80
     * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata.
81
     *
82
     * @param \ReflectionClass $class
83
     *
84
     * @return array
85
     * @throws MissingDocumentAnnotationException
86
     */
87
    public function parse(\ReflectionClass $class)
88
    {
89
        /** @var Document $document */
90
        $document = $this->reader->getClassAnnotation($class, self::DOCUMENT_ANNOTATION);
91
92
        if ($document === null) {
93
            throw new MissingDocumentAnnotationException(
94
                sprintf(
95
                    '"%s" class cannot be parsed as document because @Document annotation is missing.',
96
                    $class->getName()
97
                )
98
            );
99
        }
100
101
        $fields = [];
102
103
        return [
104
            'type' => $document->type ?: Caser::snake($class->getShortName()),
105
            'properties' => $this->getProperties($class),
106
            'fields' => array_filter(
107
                array_merge(
108
                    $document->dump(),
109
                    $fields
110
                )
111
            ),
112
            'aliases' => $this->getAliases($class, $fields),
113
            'analyzers' => $this->getAnalyzers($class),
114
            'objects' => $this->getObjects(),
115
            'namespace' => $class->getName(),
116
            'class' => $class->getShortName(),
117
        ];
118
    }
119
120
    /**
121
     * Returns document annotation data from reader.
122
     *
123
     * @param \ReflectionClass $document
124
     *
125
     * @return Document|object|null
126
     */
127
    private function getDocumentAnnotationData($document)
128
    {
129
        return $this->reader->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
130
    }
131
132
    /**
133
     * Returns property annotation data from reader.
134
     *
135
     * @param \ReflectionProperty $property
136
     *
137
     * @return Property|object|null
138
     */
139 View Code Duplication
    private function getPropertyAnnotationData(\ReflectionProperty $property)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
140
    {
141
        $result = $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION);
142
143
        if ($result !== null && $result->name === null) {
144
            $result->name = Caser::snake($property->getName());
145
        }
146
147
        return $result;
148
    }
149
150
    /**
151
     * Returns Embedded annotation data from reader.
152
     *
153
     * @param \ReflectionProperty $property
154
     *
155
     * @return Embedded|object|null
156
     */
157 View Code Duplication
    private function getEmbeddedAnnotationData(\ReflectionProperty $property)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
    {
159
        $result = $this->reader->getPropertyAnnotation($property, self::EMBEDDED_ANNOTATION);
160
161
        if ($result !== null && $result->name === null) {
162
            $result->name = Caser::snake($property->getName());
163
        }
164
165
        return $result;
166
    }
167
168
    /**
169
     * Returns HashMap annotation data from reader.
170
     *
171
     * @param \ReflectionProperty $property
172
     *
173
     * @return HashMap|object|null
174
     */
175 View Code Duplication
    private function getHashMapAnnotationData(\ReflectionProperty $property)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
    {
177
        $result = $this->reader->getPropertyAnnotation($property, self::HASH_MAP_ANNOTATION);
178
179
        if ($result !== null && $result->name === null) {
180
            $result->name = Caser::snake($property->getName());
181
        }
182
183
        return $result;
184
    }
185
186
    /**
187
     * Returns meta field annotation data from reader.
188
     *
189
     * @param \ReflectionProperty $property
190
     *
191
     * @return array
192
     */
193
    private function getMetaFieldAnnotationData($property)
194
    {
195
        /** @var MetaField $annotation */
196
        $annotation = $this->reader->getPropertyAnnotation($property, self::ID_ANNOTATION);
197
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::PARENT_ANNOTATION);
198
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::ROUTING_ANNOTATION);
199
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::VERSION_ANNOTATION);
200
201
        if ($annotation === null) {
202
            return null;
203
        }
204
205
        $data = [
206
            'name' => $annotation->getName(),
207
            'settings' => $annotation->getSettings(),
208
        ];
209
210
        if ($annotation instanceof ParentDocument) {
211
            $data['settings']['type'] = $this->getDocumentType($annotation->class);
212
        }
213
214
        return $data;
215
    }
216
217
    /**
218
     * Returns objects used in document.
219
     *
220
     * @return array
221
     */
222
    private function getObjects()
223
    {
224
        return array_keys($this->objects);
225
    }
226
227
    /**
228
     * Finds aliases for every property used in document including parent classes.
229
     *
230
     * @param \ReflectionClass $reflectionClass
231
     * @param array            $metaFields
232
     *
233
     * @return array
234
     */
235
    private function getAliases(\ReflectionClass $reflectionClass, array &$metaFields = null)
236
    {
237
        $reflectionName = $reflectionClass->getName();
238
239
        // We skip cache in case $metaFields is given. This should not affect performance
240
        // because for each document this method is called only once. For objects it might
241
        // be called few times.
242
        if ($metaFields === null && array_key_exists($reflectionName, $this->aliases)) {
243
            return $this->aliases[$reflectionName];
244
        }
245
246
        $alias = [];
247
248
        /** @var \ReflectionProperty[] $properties */
249
        $properties = $this->getDocumentPropertiesReflection($reflectionClass);
250
251
        foreach ($properties as $name => $property) {
252
            $type = $this->getPropertyAnnotationData($property);
253
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
254
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
255
256
            if ($type === null && $metaFields !== null
257
                && ($metaData = $this->getMetaFieldAnnotationData($property)) !== null) {
258
                $metaFields[$metaData['name']] = $metaData['settings'];
259
                $type = new \stdClass();
260
                $type->name = $metaData['name'];
261
            }
262
            if ($type !== null) {
263
                $alias[$type->name] = [
264
                    'propertyName' => $name,
265
                ];
266
267
                if ($type instanceof Property) {
268
                    $alias[$type->name]['type'] = $type->type;
269
                }
270
271
                if ($type instanceof HashMap) {
272
                    $alias[$type->name]['type'] = HashMap::NAME;
273
                    $alias[$type->name]['aliases'] = [];
274
                }
275
276
                $alias[$type->name][HashMap::NAME] = $type instanceof HashMap;
277
278
                switch (true) {
279
                    case $property->isPublic():
280
                        $propertyType = 'public';
281
                        break;
282
                    case $property->isProtected():
283
                    case $property->isPrivate():
284
                        $propertyType = 'private';
285
                        $alias[$type->name]['methods'] = $this->getMutatorMethods(
286
                            $reflectionClass,
287
                            $name,
288
                            $type instanceof Property ? $type->type : null
289
                        );
290
                        break;
291
                    default:
292
                        $message = sprintf(
293
                            'Wrong property %s type of %s class types cannot '.
294
                            'be static or abstract.',
295
                            $name,
296
                            $reflectionName
297
                        );
298
                        throw new \LogicException($message);
299
                }
300
                $alias[$type->name]['propertyType'] = $propertyType;
301
302
                if ($type instanceof Embedded) {
303
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class));
304
                    $alias[$type->name] = array_merge(
305
                        $alias[$type->name],
306
                        [
307
                            'type' => $this->getObjectMapping($type->class)['type'],
308
                            'multiple' => $type->multiple,
309
                            'aliases' => $this->getAliases($child),
310
                            'namespace' => $child->getName(),
311
                        ]
312
                    );
313
                }
314
            }
315
        }
316
317
        $this->aliases[$reflectionName] = $alias;
318
319
        return $this->aliases[$reflectionName];
320
    }
321
322
    /**
323
     * Checks if class have setter and getter, and returns them in array.
324
     *
325
     * @param \ReflectionClass $reflectionClass
326
     * @param string           $property
327
     *
328
     * @return array
329
     */
330
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
331
    {
332
        $camelCaseName = ucfirst(Caser::camel($property));
333
        $setterName = 'set'.$camelCaseName;
334
        if (!$reflectionClass->hasMethod($setterName)) {
335
            $message = sprintf(
336
                'Missing %s() method in %s class. Add it, or change property to public.',
337
                $setterName,
338
                $reflectionClass->getName()
339
            );
340
            throw new \LogicException($message);
341
        }
342
343 View Code Duplication
        if ($reflectionClass->hasMethod('get'.$camelCaseName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
            return [
345
                'getter' => 'get' . $camelCaseName,
346
                'setter' => $setterName
347
            ];
348
        }
349
350
        if ($propertyType === 'boolean') {
351 View Code Duplication
            if ($reflectionClass->hasMethod('is' . $camelCaseName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
                return [
353
                    'getter' => 'is' . $camelCaseName,
354
                    'setter' => $setterName
355
                ];
356
            }
357
358
            $message = sprintf(
359
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
360
                'get'.$camelCaseName,
361
                'is'.$camelCaseName,
362
                $reflectionClass->getName()
363
            );
364
            throw new \LogicException($message);
365
        }
366
367
        $message = sprintf(
368
            'Missing %s() method in %s class. Add it, or change property to public.',
369
            'get'.$camelCaseName,
370
            $reflectionClass->getName()
371
        );
372
        throw new \LogicException($message);
373
    }
374
375
    /**
376
     * Registers annotations to registry so that it could be used by reader.
377
     */
378
    private function registerAnnotations()
379
    {
380
        $annotations = [
381
            'Document',
382
            'Property',
383
            'Embedded',
384
            'Object',
385
            'Nested',
386
            'Id',
387
            'ParentDocument',
388
            'Routing',
389
            'Version',
390
            'HashMap',
391
        ];
392
393
        foreach ($annotations as $annotation) {
394
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
395
        }
396
    }
397
398
    /**
399
     * Returns document type.
400
     *
401
     * @param string $document Format must be like AcmeBundle:Document.
402
     *
403
     * @return string
404
     */
405
    private function getDocumentType($document)
406
    {
407
        $namespace = $this->finder->getNamespace($document);
408
        $reflectionClass = new \ReflectionClass($namespace);
409
        $document = $this->getDocumentAnnotationData($reflectionClass);
410
411
        return empty($document->type) ? Caser::snake($reflectionClass->getShortName()) : $document->type;
412
    }
413
414
    /**
415
     * Returns all defined properties including private from parents.
416
     *
417
     * @param \ReflectionClass $reflectionClass
418
     *
419
     * @return array
420
     */
421
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
422
    {
423
        if (in_array($reflectionClass->getName(), $this->properties)) {
424
            return $this->properties[$reflectionClass->getName()];
425
        }
426
427
        $properties = [];
428
429
        foreach ($reflectionClass->getProperties() as $property) {
430
            if (!in_array($property->getName(), $properties)) {
431
                $properties[$property->getName()] = $property;
432
            }
433
        }
434
435
        $parentReflection = $reflectionClass->getParentClass();
436
        if ($parentReflection !== false) {
437
            $properties = array_merge(
438
                $properties,
439
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
440
            );
441
        }
442
443
        $this->properties[$reflectionClass->getName()] = $properties;
444
445
        return $properties;
446
    }
447
448
    /**
449
     * Parses analyzers list from document mapping.
450
     *
451
     * @param \ReflectionClass $reflectionClass
452
     * @return array
453
     */
454
    private function getAnalyzers(\ReflectionClass $reflectionClass)
455
    {
456
        $analyzers = [];
457
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
458
            $type = $this->getPropertyAnnotationData($property);
459
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
460
461
            if ($type instanceof Embedded) {
462
                $analyzers = array_merge(
463
                    $analyzers,
464
                    $this->getAnalyzers(new \ReflectionClass($this->finder->getNamespace($type->class)))
465
                );
466
            }
467
468
            if ($type instanceof Property) {
469
                if (isset($type->options['analyzer'])) {
470
                    $analyzers[] = $type->options['analyzer'];
471
                }
472
                if (isset($type->options['search_analyzer'])) {
473
                    $analyzers[] = $type->options['search_analyzer'];
474
                }
475
476
                if (isset($type->options['fields'])) {
477
                    foreach ($type->options['fields'] as $field) {
478
                        if (isset($field['analyzer'])) {
479
                            $analyzers[] = $field['analyzer'];
480
                        }
481
                        if (isset($field['search_analyzer'])) {
482
                            $analyzers[] = $field['search_analyzer'];
483
                        }
484
                    }
485
                }
486
            }
487
        }
488
        return array_unique($analyzers);
489
    }
490
491
    /**
492
     * Returns properties of reflection class.
493
     *
494
     * @param \ReflectionClass $reflectionClass Class to read properties from.
495
     * @param array            $properties      Properties to skip.
496
     * @param bool             $flag            If false exludes properties, true only includes properties.
497
     *
498
     * @return array
499
     */
500
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
501
    {
502
        $mapping = [];
503
        /** @var \ReflectionProperty $property */
504
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
505
            $type = $this->getPropertyAnnotationData($property);
506
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
507
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
508
509
            if ((in_array($name, $properties) && !$flag)
510
                || (!in_array($name, $properties) && $flag)
511
                || empty($type)
512
            ) {
513
                continue;
514
            }
515
516
            $map = $type->dump();
517
518
            // Inner object
519
            if ($type instanceof Embedded) {
520
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class));
521
            }
522
523
            // HashMap object
524
            if ($type instanceof HashMap) {
525
                $map = array_replace_recursive($map, [
526
                    'type' => Nested::NAME,
527
                    'dynamic' => true,
528
                ]);
529
            }
530
531
            // If there is set some Raw options, it will override current ones.
532
            if (isset($map['options'])) {
533
                $options = $map['options'];
534
                unset($map['options']);
535
                $map = array_merge($map, $options);
536
            }
537
538
            $mapping[$type->name] = $map;
539
        }
540
541
        return $mapping;
542
    }
543
544
    /**
545
     * Returns object mapping.
546
     *
547
     * Loads from cache if it's already loaded.
548
     *
549
     * @param string $className
550
     *
551
     * @return array
552
     */
553
    private function getObjectMapping($className)
554
    {
555
        $namespace = $this->finder->getNamespace($className);
556
557
        if (array_key_exists($namespace, $this->objects)) {
558
            return $this->objects[$namespace];
559
        }
560
561
        $reflectionClass = new \ReflectionClass($namespace);
562
563
        switch (true) {
564
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
565
                $type = 'object';
566
                break;
567
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
568
                $type = 'nested';
569
                break;
570
            default:
571
                throw new \LogicException(
572
                    sprintf(
573
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
574
                        $className
575
                    )
576
                );
577
        }
578
579
        $this->objects[$namespace] = [
580
            'type' => $type,
581
            'properties' => $this->getProperties($reflectionClass),
582
        ];
583
584
        return $this->objects[$namespace];
585
    }
586
}
587