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