Completed
Pull Request — 5.0 (#826)
by Toni
01:38
created

DocumentParser   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 594
Duplicated Lines 7.07 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 11
dl 42
loc 594
rs 4.5142
c 0
b 0
f 0

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
     * @var array Local cache for documents
70
     */
71
    private $documents = [];
72
73
    /**
74
     * @param Reader         $reader Used for reading annotations.
75
     * @param DocumentFinder $finder Used for resolving namespaces.
76
     */
77
    public function __construct(Reader $reader, DocumentFinder $finder)
78
    {
79
        $this->reader = $reader;
80
        $this->finder = $finder;
81
        $this->registerAnnotations();
82
    }
83
84
    /**
85
     * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata.
86
     *
87
     * @param \ReflectionClass $class
88
     *
89
     * @return array|null
90
     * @throws MissingDocumentAnnotationException
91
     */
92
    public function parse(\ReflectionClass $class)
93
    {
94
        $className = $class->getName();
95
96
        if ($class->isTrait()) {
97
            return false;
98
        }
99
100
        if (!isset($this->documents[$className])) {
101
            /** @var Document $document */
102
            $document = $this->reader->getClassAnnotation($class, self::DOCUMENT_ANNOTATION);
103
104
            if ($document === null) {
105
                throw new MissingDocumentAnnotationException(
106
                    sprintf(
107
                        '"%s" class cannot be parsed as document because @Document annotation is missing.',
108
                        $class->getName()
109
                    )
110
                );
111
            }
112
113
            $fields = [];
114
            $aliases = $this->getAliases($class, $fields)
115
116
            $this->documents[$className] = [
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_VARIABLE
Loading history...
117
                'type' => $document->type ?: Caser::snake($class->getShortName()),
118
                'properties' => $this->getProperties($class),
119
                'fields' => array_filter(
120
                    array_merge(
121
                        $document->dump(),
122
                        $fields
123
                    )
124
                ),
125
                'aliases' => $aliases,
126
                'analyzers' => $this->getAnalyzers($class),
127
                'objects' => $this->getObjects(),
128
                'namespace' => $class->getName(),
129
                'class' => $class->getShortName(),
130
            ];
131
        }
132
        return $this->documents[$className];
133
    }
134
135
    /**
136
     * Returns document annotation data from reader.
137
     *
138
     * @param \ReflectionClass $document
139
     *
140
     * @return Document|object|null
141
     */
142
    private function getDocumentAnnotationData($document)
143
    {
144
        return $this->reader->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
145
    }
146
147
    /**
148
     * Returns property annotation data from reader.
149
     *
150
     * @param \ReflectionProperty $property
151
     *
152
     * @return Property|object|null
153
     */
154
    private function getPropertyAnnotationData(\ReflectionProperty $property)
155
    {
156
        $result = $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION);
157
158
        if ($result !== null && $result->name === null) {
159
            $result->name = Caser::snake($property->getName());
160
        }
161
162
        return $result;
163
    }
164
165
    /**
166
     * Returns Embedded annotation data from reader.
167
     *
168
     * @param \ReflectionProperty $property
169
     *
170
     * @return Embedded|object|null
171
     */
172
    private function getEmbeddedAnnotationData(\ReflectionProperty $property)
173
    {
174
        $result = $this->reader->getPropertyAnnotation($property, self::EMBEDDED_ANNOTATION);
175
176
        if ($result !== null && $result->name === null) {
177
            $result->name = Caser::snake($property->getName());
178
        }
179
180
        return $result;
181
    }
182
183
    /**
184
     * Returns HashMap annotation data from reader.
185
     *
186
     * @param \ReflectionProperty $property
187
     *
188
     * @return HashMap|object|null
189
     */
190
    private function getHashMapAnnotationData(\ReflectionProperty $property)
191
    {
192
        $result = $this->reader->getPropertyAnnotation($property, self::HASH_MAP_ANNOTATION);
193
194
        if ($result !== null && $result->name === null) {
195
            $result->name = Caser::snake($property->getName());
196
        }
197
198
        return $result;
199
    }
200
201
    /**
202
     * Returns meta field annotation data from reader.
203
     *
204
     * @param \ReflectionProperty $property
205
     * @param string              $directory The name of the Document directory in the bundle
206
     *
207
     * @return array
208
     */
209
    private function getMetaFieldAnnotationData($property, $directory)
210
    {
211
        /** @var MetaField $annotation */
212
        $annotation = $this->reader->getPropertyAnnotation($property, self::ID_ANNOTATION);
213
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::PARENT_ANNOTATION);
214
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::ROUTING_ANNOTATION);
215
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::VERSION_ANNOTATION);
216
217
        if ($annotation === null) {
218
            return null;
219
        }
220
221
        $data = [
222
            'name' => $annotation->getName(),
223
            'settings' => $annotation->getSettings(),
224
        ];
225
226
        if ($annotation instanceof ParentDocument) {
227
            $data['settings']['type'] = $this->getDocumentType($annotation->class, $directory);
228
        }
229
230
        return $data;
231
    }
232
233
    /**
234
     * Returns objects used in document.
235
     *
236
     * @return array
237
     */
238
    private function getObjects()
239
    {
240
        return array_keys($this->objects);
241
    }
242
243
    /**
244
     * Finds aliases for every property used in document including parent classes.
245
     *
246
     * @param \ReflectionClass $reflectionClass
247
     * @param array            $metaFields
248
     *
249
     * @return array
250
     */
251
    private function getAliases(\ReflectionClass $reflectionClass, array &$metaFields = null)
252
    {
253
        $reflectionName = $reflectionClass->getName();
254
        $directory = $this->guessDirName($reflectionClass);
255
256
        // We skip cache in case $metaFields is given. This should not affect performance
257
        // because for each document this method is called only once. For objects it might
258
        // be called few times.
259
        if ($metaFields === null && array_key_exists($reflectionName, $this->aliases)) {
260
            return $this->aliases[$reflectionName];
261
        }
262
263
        $alias = [];
264
265
        /** @var \ReflectionProperty[] $properties */
266
        $properties = $this->getDocumentPropertiesReflection($reflectionClass);
267
268
        foreach ($properties as $name => $property) {
269
            $type = $this->getPropertyAnnotationData($property);
270
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
271
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
272
273
            if ($type === null && $metaFields !== null
274
                && ($metaData = $this->getMetaFieldAnnotationData($property, $directory)) !== null) {
275
                $metaFields[$metaData['name']] = $metaData['settings'];
276
                $type = new \stdClass();
277
                $type->name = $metaData['name'];
278
            }
279
            if ($type !== null) {
280
                $alias[$type->name] = [
281
                    'propertyName' => $name,
282
                ];
283
284
                if ($type instanceof Property) {
285
                    $alias[$type->name]['type'] = $type->type;
286
                }
287
288
                if ($type instanceof HashMap) {
289
                    $alias[$type->name]['type'] = HashMap::NAME;
290
                }
291
292
                $alias[$type->name][HashMap::NAME] = $type instanceof HashMap;
293
294
                switch (true) {
295
                    case $property->isPublic():
296
                        $propertyType = 'public';
297
                        break;
298
                    case $property->isProtected():
299
                    case $property->isPrivate():
300
                        $propertyType = 'private';
301
                        $alias[$type->name]['methods'] = $this->getMutatorMethods(
302
                            $reflectionClass,
303
                            $name,
304
                            $type instanceof Property ? $type->type : null
305
                        );
306
                        break;
307
                    default:
308
                        $message = sprintf(
309
                            'Wrong property %s type of %s class types cannot '.
310
                            'be static or abstract.',
311
                            $name,
312
                            $reflectionName
313
                        );
314
                        throw new \LogicException($message);
315
                }
316
                $alias[$type->name]['propertyType'] = $propertyType;
317
318
                if ($type instanceof Embedded) {
319
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class, $directory));
320
                    $alias[$type->name] = array_merge(
321
                        $alias[$type->name],
322
                        [
323
                            'type' => $this->getObjectMapping($type->class, $directory)['type'],
324
                            'multiple' => $type->multiple,
325
                            'aliases' => $this->getAliases($child, $metaFields),
326
                            'namespace' => $child->getName(),
327
                        ]
328
                    );
329
                }
330
            }
331
        }
332
333
        $this->aliases[$reflectionName] = $alias;
334
335
        return $this->aliases[$reflectionName];
336
    }
337
338
    /**
339
     * Checks if class have setter and getter, and returns them in array.
340
     *
341
     * @param \ReflectionClass $reflectionClass
342
     * @param string           $property
343
     *
344
     * @return array
345
     */
346
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
347
    {
348
        $camelCaseName = ucfirst(Caser::camel($property));
349
        $setterName = 'set'.$camelCaseName;
350
        if (!$reflectionClass->hasMethod($setterName)) {
351
            $message = sprintf(
352
                'Missing %s() method in %s class. Add it, or change property to public.',
353
                $setterName,
354
                $reflectionClass->getName()
355
            );
356
            throw new \LogicException($message);
357
        }
358
359
        if ($reflectionClass->hasMethod('get'.$camelCaseName)) {
360
            return [
361
                'getter' => 'get' . $camelCaseName,
362
                'setter' => $setterName
363
            ];
364
        }
365
366
        if ($propertyType === 'boolean') {
367
            if ($reflectionClass->hasMethod('is' . $camelCaseName)) {
368
                return [
369
                    'getter' => 'is' . $camelCaseName,
370
                    'setter' => $setterName
371
                ];
372
            }
373
374
            $message = sprintf(
375
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
376
                'get'.$camelCaseName,
377
                'is'.$camelCaseName,
378
                $reflectionClass->getName()
379
            );
380
            throw new \LogicException($message);
381
        }
382
383
        $message = sprintf(
384
            'Missing %s() method in %s class. Add it, or change property to public.',
385
            'get'.$camelCaseName,
386
            $reflectionClass->getName()
387
        );
388
        throw new \LogicException($message);
389
    }
390
391
    /**
392
     * Registers annotations to registry so that it could be used by reader.
393
     */
394
    private function registerAnnotations()
395
    {
396
        $annotations = [
397
            'Document',
398
            'Property',
399
            'Embedded',
400
            'Object',
401
            'Nested',
402
            'Id',
403
            'ParentDocument',
404
            'Routing',
405
            'Version',
406
            'HashMap',
407
        ];
408
409
        foreach ($annotations as $annotation) {
410
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
411
        }
412
    }
413
414
    /**
415
     * Returns document type.
416
     *
417
     * @param string $document  Format must be like AcmeBundle:Document.
418
     * @param string $directory The Document directory name of the bundle.
419
     *
420
     * @return string
421
     */
422
    private function getDocumentType($document, $directory)
423
    {
424
        $namespace = $this->finder->getNamespace($document, $directory);
425
        $reflectionClass = new \ReflectionClass($namespace);
426
        $document = $this->getDocumentAnnotationData($reflectionClass);
427
428
        return empty($document->type) ? Caser::snake($reflectionClass->getShortName()) : $document->type;
429
    }
430
431
    /**
432
     * Returns all defined properties including private from parents.
433
     *
434
     * @param \ReflectionClass $reflectionClass
435
     *
436
     * @return array
437
     */
438
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
439
    {
440
        if (in_array($reflectionClass->getName(), $this->properties)) {
441
            return $this->properties[$reflectionClass->getName()];
442
        }
443
444
        $properties = [];
445
446
        foreach ($reflectionClass->getProperties() as $property) {
447
            if (!in_array($property->getName(), $properties)) {
448
                $properties[$property->getName()] = $property;
449
            }
450
        }
451
452
        $parentReflection = $reflectionClass->getParentClass();
453
        if ($parentReflection !== false) {
454
            $properties = array_merge(
455
                $properties,
456
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
457
            );
458
        }
459
460
        $this->properties[$reflectionClass->getName()] = $properties;
461
462
        return $properties;
463
    }
464
465
    /**
466
     * Parses analyzers list from document mapping.
467
     *
468
     * @param \ReflectionClass $reflectionClass
469
     * @return array
470
     */
471
    private function getAnalyzers(\ReflectionClass $reflectionClass)
472
    {
473
        $analyzers = [];
474
        $directory = $this->guessDirName($reflectionClass);
475
476
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
477
            $type = $this->getPropertyAnnotationData($property);
478
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
479
480
            if ($type instanceof Embedded) {
481
                $analyzers = array_merge(
482
                    $analyzers,
483
                    $this->getAnalyzers(new \ReflectionClass($this->finder->getNamespace($type->class, $directory)))
484
                );
485
            }
486
487
            if ($type instanceof Property) {
488
                if (isset($type->options['analyzer'])) {
489
                    $analyzers[] = $type->options['analyzer'];
490
                }
491
                if (isset($type->options['search_analyzer'])) {
492
                    $analyzers[] = $type->options['search_analyzer'];
493
                }
494
495
                if (isset($type->options['fields'])) {
496
                    foreach ($type->options['fields'] as $field) {
497
                        if (isset($field['analyzer'])) {
498
                            $analyzers[] = $field['analyzer'];
499
                        }
500
                        if (isset($field['search_analyzer'])) {
501
                            $analyzers[] = $field['search_analyzer'];
502
                        }
503
                    }
504
                }
505
            }
506
        }
507
        return array_unique($analyzers);
508
    }
509
510
    /**
511
     * Returns properties of reflection class.
512
     *
513
     * @param \ReflectionClass $reflectionClass Class to read properties from.
514
     * @param array            $properties      Properties to skip.
515
     * @param bool             $flag            If false exludes properties, true only includes properties.
516
     *
517
     * @return array
518
     */
519
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
520
    {
521
        $mapping = [];
522
        $directory = $this->guessDirName($reflectionClass);
523
524
        /** @var \ReflectionProperty $property */
525
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
526
            $type = $this->getPropertyAnnotationData($property);
527
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
528
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
529
530
            if ((in_array($name, $properties) && !$flag)
531
                || (!in_array($name, $properties) && $flag)
532
                || empty($type)
533
            ) {
534
                continue;
535
            }
536
537
            $map = $type->dump();
538
539
            // Inner object
540
            if ($type instanceof Embedded) {
541
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class, $directory));
542
            }
543
544
            // HashMap object
545
            if ($type instanceof HashMap) {
546
                $map = array_replace_recursive($map, [
547
                    'type' => Nested::NAME,
548
                    'dynamic' => true,
549
                ]);
550
            }
551
552
            // If there is set some Raw options, it will override current ones.
553
            if (isset($map['options'])) {
554
                $options = $map['options'];
555
                unset($map['options']);
556
                $map = array_merge($map, $options);
557
            }
558
559
            $mapping[$type->name] = $map;
560
        }
561
562
        return $mapping;
563
    }
564
565
    /**
566
     * Returns object mapping.
567
     *
568
     * Loads from cache if it's already loaded.
569
     *
570
     * @param string $className
571
     * @param string $directory Name of the directory where the Document is
572
     *
573
     * @return array
574
     */
575
    private function getObjectMapping($className, $directory)
576
    {
577
        $namespace = $this->finder->getNamespace($className, $directory);
578
579
        if (array_key_exists($namespace, $this->objects)) {
580
            return $this->objects[$namespace];
581
        }
582
583
        $reflectionClass = new \ReflectionClass($namespace);
584
585
        switch (true) {
586
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
587
                $type = 'object';
588
                break;
589
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
590
                $type = 'nested';
591
                break;
592
            default:
593
                throw new \LogicException(
594
                    sprintf(
595
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
596
                        $className
597
                    )
598
                );
599
        }
600
601
        $this->objects[$namespace] = [
602
            'type' => $type,
603
            'properties' => $this->getProperties($reflectionClass),
604
        ];
605
606
        return $this->objects[$namespace];
607
    }
608
609
    /**
610
     * @param \ReflectionClass $reflection
611
     *
612
     * @return string
613
     */
614
    private function guessDirName(\ReflectionClass $reflection)
615
    {
616
        return substr(
617
            $directory = $reflection->getName(),
618
            $start = strpos($directory, '\\') + 1,
619
            strrpos($directory, '\\') - $start
620
        );
621
    }
622
}
623