Completed
Pull Request — 1.0 (#637)
by
unknown
02:32
created

DocumentParser::getProperties()   D

Complexity

Conditions 10
Paths 11

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 34
rs 4.8196
cc 10
eloc 18
nc 11
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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