Completed
Push — master ( 973a09...0923d3 )
by Simonas
02:31
created

DocumentParser   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 399
Duplicated Lines 3.01 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 8
Bugs 1 Features 1
Metric Value
wmc 47
c 8
b 1
f 1
lcom 1
cbo 6
dl 12
loc 399
rs 8.439

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getDocumentAnnotationData() 0 4 1
A getPropertyAnnotationData() 0 4 1
A getObjects() 0 4 1
B parse() 0 40 5
C getAliases() 0 58 9
B getMutatorMethods() 12 44 5
A registerAnnotations() 0 13 2
A getDocumentType() 0 8 2
B getDocumentPropertiesReflection() 0 26 5
D getProperties() 0 33 10
A getObjectMapping() 0 12 2
A getRelationMapping() 0 10 3

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\Property;
18
19
/**
20
 * Document parser used for reading document annotations.
21
 */
22
class DocumentParser
23
{
24
    /**
25
     * @const string
26
     */
27
    const PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Property';
28
29
    /**
30
     * @const string
31
     */
32
    const DOCUMENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Document';
33
34
    /**
35
     * @var Reader Used to read document annotations.
36
     */
37
    private $reader;
38
39
    /**
40
     * @var DocumentFinder Used to find documents.
41
     */
42
    private $finder;
43
44
    /**
45
     * @var array Contains gathered objects which later adds to documents.
46
     */
47
    private $objects = [];
48
49
    /**
50
     * @var array Document properties aliases.
51
     */
52
    private $aliases = [];
53
54
    /**
55
     * @var array Local cache for document properties.
56
     */
57
    private $properties = [];
58
59
    /**
60
     * @param Reader         $reader Used for reading annotations.
61
     * @param DocumentFinder $finder Used for resolving namespaces.
62
     */
63
    public function __construct(Reader $reader, DocumentFinder $finder)
64
    {
65
        $this->reader = $reader;
66
        $this->finder = $finder;
67
        $this->registerAnnotations();
68
    }
69
70
    /**
71
     * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata.
72
     *
73
     * @param \ReflectionClass $document
74
     *
75
     * @return array
76
     */
77
    public function parse(\ReflectionClass $document)
78
    {
79
        /** @var Document $class */
80
        $class = $this
81
            ->reader
82
            ->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
83
84
        if ($class !== null) {
85
            if ($class->parent !== null) {
86
                $parent = $this->getDocumentType($class->parent);
87
            } else {
88
                $parent = null;
89
            }
90
91
            $properties = $this->getProperties($document);
92
93
            if ($class->type) {
94
                $documentType = $class->type;
95
            } else {
96
                $documentType = $document->getShortName();
97
            }
98
99
            return [
100
                'type' => $documentType,
101
                'properties' => $properties,
102
                'fields' => array_filter(
103
                    array_merge(
104
                        $class->dump(),
105
                        ['_parent' => $parent === null ? null : ['type' => $parent]]
106
                    )
107
                ),
108
                'aliases' => $this->getAliases($document),
109
                'objects' => $this->getObjects(),
110
                'namespace' => $document->getName(),
111
                'class' => $document->getShortName(),
112
            ];
113
        }
114
115
        return [];
116
    }
117
118
    /**
119
     * Returns document annotation data from reader.
120
     *
121
     * @param \ReflectionClass $document
122
     *
123
     * @return Document|null
124
     */
125
    public function getDocumentAnnotationData($document)
126
    {
127
        return $this->reader->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
128
    }
129
130
    /**
131
     * Returns property annotation data from reader.
132
     *
133
     * @param \ReflectionProperty $property
134
     *
135
     * @return Property|null
136
     */
137
    public function getPropertyAnnotationData($property)
138
    {
139
        return $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION);
140
    }
141
142
    /**
143
     * Returns objects used in document.
144
     *
145
     * @return array
146
     */
147
    private function getObjects()
148
    {
149
        return array_keys($this->objects);
150
    }
151
152
    /**
153
     * Finds aliases for every property used in document including parent classes.
154
     *
155
     * @param \ReflectionClass $reflectionClass
156
     *
157
     * @return array
158
     */
159
    private function getAliases(\ReflectionClass $reflectionClass)
160
    {
161
        $reflectionName = $reflectionClass->getName();
162
        if (array_key_exists($reflectionName, $this->aliases)) {
163
            return $this->aliases[$reflectionName];
164
        }
165
166
        $alias = [];
167
168
        /** @var \ReflectionProperty[] $properties */
169
        $properties = $this->getDocumentPropertiesReflection($reflectionClass);
170
171
        foreach ($properties as $name => $property) {
172
            $type = $this->getPropertyAnnotationData($property);
173
            if ($type !== null) {
174
                $alias[$type->name] = [
175
                    'propertyName' => $name,
176
                    'type' => $type->type,
177
                ];
178
                switch (true) {
179
                    case $property->isPublic():
180
                        $propertyType = 'public';
181
                        break;
182
                    case $property->isProtected():
183
                    case $property->isPrivate():
184
                        $propertyType = 'private';
185
                        $alias[$type->name]['methods'] = $this->getMutatorMethods($reflectionClass, $name, $type->type);
186
                        break;
187
                    default:
188
                        $message = sprintf(
189
                            'Wrong property %s type of %s class types cannot '.
190
                            'be static or abstract.',
191
                            $name,
192
                            $reflectionName
193
                        );
194
                        throw new \LogicException($message);
195
                }
196
                $alias[$type->name]['propertyType'] = $propertyType;
197
198
199
                if ($type->objectName) {
200
                    $child = new \ReflectionClass($this->finder->getNamespace($type->objectName));
201
                    $alias[$type->name] = array_merge(
202
                        $alias[$type->name],
203
                        [
204
                            'multiple' => $type instanceof Property ? $type->multiple : false,
205
                            'aliases' => $this->getAliases($child),
206
                            'namespace' => $child->getName(),
207
                        ]
208
                    );
209
                }
210
            }
211
        }
212
213
        $this->aliases[$reflectionName] = $alias;
214
215
        return $this->aliases[$reflectionName];
216
    }
217
218
    /**
219
     * Checks if class have setter and getter, and returns them in array.
220
     *
221
     * @param \ReflectionClass $reflectionClass
222
     * @param string           $property
223
     *
224
     * @return array
225
     */
226
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
227
    {
228
        $camelCaseName = ucfirst(Caser::camel($property));
229
        $setterName = 'set'.$camelCaseName;
230
        if (!$reflectionClass->hasMethod($setterName)) {
231
            $message = sprintf(
232
                'Missing %s() method in %s class. Add it, or change property to public.',
233
                $setterName,
234
                $reflectionClass->getName()
235
            );
236
            throw new \LogicException($message);
237
        }
238
239 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...
240
            return [
241
                'getter' => 'get' . $camelCaseName,
242
                'setter' => $setterName
243
            ];
244
        }
245
246
        if ($propertyType === 'boolean') {
247 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...
248
                return [
249
                    'getter' => 'is' . $camelCaseName,
250
                    'setter' => $setterName
251
                ];
252
            }
253
254
            $message = sprintf(
255
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
256
                'get'.$camelCaseName,
257
                'is'.$camelCaseName,
258
                $reflectionClass->getName()
259
            );
260
            throw new \LogicException($message);
261
        }
262
263
        $message = sprintf(
264
            'Missing %s() method in %s class. Add it, or change property to public.',
265
            'get'.$camelCaseName,
266
            $reflectionClass->getName()
267
        );
268
        throw new \LogicException($message);
269
    }
270
271
    /**
272
     * Registers annotations to registry so that it could be used by reader.
273
     */
274
    private function registerAnnotations()
275
    {
276
        $annotations = [
277
            'Document',
278
            'Property',
279
            'Object',
280
            'Nested',
281
        ];
282
283
        foreach ($annotations as $annotation) {
284
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
285
        }
286
    }
287
288
    /**
289
     * Returns document type.
290
     *
291
     * @param string $document Format must be like AcmeBundle:Document.
292
     *
293
     * @return string
294
     */
295
    private function getDocumentType($document)
296
    {
297
        $namespace = $this->finder->getNamespace($document);
298
        $reflectionClass = new \ReflectionClass($namespace);
299
        $document = $this->getDocumentAnnotationData($reflectionClass);
300
301
        return empty($document->type) ? $reflectionClass->getShortName() : $document->type;
302
    }
303
304
    /**
305
     * Returns all defined properties including private from parents.
306
     *
307
     * @param \ReflectionClass $reflectionClass
308
     *
309
     * @return array
310
     */
311
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
312
    {
313
        if (in_array($reflectionClass->getName(), $this->properties)) {
314
            return $this->properties[$reflectionClass->getName()];
315
        }
316
317
        $properties = [];
318
319
        foreach ($reflectionClass->getProperties() as $property) {
320
            if (!in_array($property->getName(), $properties)) {
321
                $properties[$property->getName()] = $property;
322
            }
323
        }
324
325
        $parentReflection = $reflectionClass->getParentClass();
326
        if ($parentReflection !== false) {
327
            $properties = array_merge(
328
                $properties,
329
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
330
            );
331
        }
332
333
        $this->properties[$reflectionClass->getName()] = $properties;
334
335
        return $properties;
336
    }
337
338
    /**
339
     * Returns properties of reflection class.
340
     *
341
     * @param \ReflectionClass $reflectionClass Class to read properties from.
342
     * @param array            $properties      Properties to skip.
343
     * @param bool             $flag            If false exludes properties, true only includes properties.
344
     *
345
     * @return array
346
     */
347
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
348
    {
349
        $mapping = [];
350
        /** @var \ReflectionProperty $property */
351
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
352
            $type = $this->getPropertyAnnotationData($property);
353
354
            if ((in_array($name, $properties) && !$flag)
355
                || (!in_array($name, $properties) && $flag)
356
                || empty($type)
357
            ) {
358
                continue;
359
            }
360
361
            $map = $type->dump();
362
363
            // Object.
364
            if (in_array($type->type, ['object', 'nested']) && !empty($type->objectName)) {
365
                $map = array_replace_recursive($map, $this->getObjectMapping($type->objectName));
366
            }
367
368
            // If there is set some Raw options, it will override current ones.
369
            if (isset($map['options'])) {
370
                $options = $map['options'];
371
                unset($map['options']);
372
                $map = array_merge($map, $options);
373
            }
374
375
            $mapping[$type->name] = $map;
376
        }
377
378
        return $mapping;
379
    }
380
381
    /**
382
     * Returns object mapping.
383
     *
384
     * Loads from cache if it's already loaded.
385
     *
386
     * @param string $objectName
387
     *
388
     * @return array
389
     */
390
    private function getObjectMapping($objectName)
391
    {
392
        $namespace = $this->finder->getNamespace($objectName);
393
394
        if (array_key_exists($namespace, $this->objects)) {
395
            return $this->objects[$namespace];
396
        }
397
398
        $this->objects[$namespace] = $this->getRelationMapping(new \ReflectionClass($namespace));
399
400
        return $this->objects[$namespace];
401
    }
402
403
    /**
404
     * Returns relation mapping by its reflection.
405
     *
406
     * @param \ReflectionClass $reflectionClass
407
     *
408
     * @return array|null
409
     */
410
    private function getRelationMapping(\ReflectionClass $reflectionClass)
411
    {
412
        if ($this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Object')
413
            || $this->reader->getClassAnnotation($reflectionClass, 'ONGR\ElasticsearchBundle\Annotation\Nested')
414
        ) {
415
            return ['properties' => $this->getProperties($reflectionClass)];
416
        }
417
418
        return null;
419
    }
420
}
421