Completed
Pull Request — master (#759)
by Mantas
02:31
created

DocumentParser::getHashMapAnnotationData()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 2
nop 1
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
                }
274
275
                $alias[$type->name][HashMap::NAME] = $type instanceof HashMap;
276
277
                switch (true) {
278
                    case $property->isPublic():
279
                        $propertyType = 'public';
280
                        break;
281
                    case $property->isProtected():
282
                    case $property->isPrivate():
283
                        $propertyType = 'private';
284
                        $alias[$type->name]['methods'] = $this->getMutatorMethods(
285
                            $reflectionClass,
286
                            $name,
287
                            $type instanceof Property ? $type->type : null
288
                        );
289
                        break;
290
                    default:
291
                        $message = sprintf(
292
                            'Wrong property %s type of %s class types cannot '.
293
                            'be static or abstract.',
294
                            $name,
295
                            $reflectionName
296
                        );
297
                        throw new \LogicException($message);
298
                }
299
                $alias[$type->name]['propertyType'] = $propertyType;
300
301
                if ($type instanceof Embedded) {
302
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class));
303
                    $alias[$type->name] = array_merge(
304
                        $alias[$type->name],
305
                        [
306
                            'type' => $this->getObjectMapping($type->class)['type'],
307
                            'multiple' => $type->multiple,
308
                            'aliases' => $this->getAliases($child),
309
                            'namespace' => $child->getName(),
310
                        ]
311
                    );
312
                }
313
            }
314
        }
315
316
        $this->aliases[$reflectionName] = $alias;
317
318
        return $this->aliases[$reflectionName];
319
    }
320
321
    /**
322
     * Checks if class have setter and getter, and returns them in array.
323
     *
324
     * @param \ReflectionClass $reflectionClass
325
     * @param string           $property
326
     *
327
     * @return array
328
     */
329
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
330
    {
331
        $camelCaseName = ucfirst(Caser::camel($property));
332
        $setterName = 'set'.$camelCaseName;
333
        if (!$reflectionClass->hasMethod($setterName)) {
334
            $message = sprintf(
335
                'Missing %s() method in %s class. Add it, or change property to public.',
336
                $setterName,
337
                $reflectionClass->getName()
338
            );
339
            throw new \LogicException($message);
340
        }
341
342 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...
343
            return [
344
                'getter' => 'get' . $camelCaseName,
345
                'setter' => $setterName
346
            ];
347
        }
348
349
        if ($propertyType === 'boolean') {
350 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...
351
                return [
352
                    'getter' => 'is' . $camelCaseName,
353
                    'setter' => $setterName
354
                ];
355
            }
356
357
            $message = sprintf(
358
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
359
                'get'.$camelCaseName,
360
                'is'.$camelCaseName,
361
                $reflectionClass->getName()
362
            );
363
            throw new \LogicException($message);
364
        }
365
366
        $message = sprintf(
367
            'Missing %s() method in %s class. Add it, or change property to public.',
368
            'get'.$camelCaseName,
369
            $reflectionClass->getName()
370
        );
371
        throw new \LogicException($message);
372
    }
373
374
    /**
375
     * Registers annotations to registry so that it could be used by reader.
376
     */
377
    private function registerAnnotations()
378
    {
379
        $annotations = [
380
            'Document',
381
            'Property',
382
            'Embedded',
383
            'Object',
384
            'Nested',
385
            'Id',
386
            'ParentDocument',
387
            'Routing',
388
            'Version',
389
            'HashMap',
390
        ];
391
392
        foreach ($annotations as $annotation) {
393
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
394
        }
395
    }
396
397
    /**
398
     * Returns document type.
399
     *
400
     * @param string $document Format must be like AcmeBundle:Document.
401
     *
402
     * @return string
403
     */
404
    private function getDocumentType($document)
405
    {
406
        $namespace = $this->finder->getNamespace($document);
407
        $reflectionClass = new \ReflectionClass($namespace);
408
        $document = $this->getDocumentAnnotationData($reflectionClass);
409
410
        return empty($document->type) ? Caser::snake($reflectionClass->getShortName()) : $document->type;
411
    }
412
413
    /**
414
     * Returns all defined properties including private from parents.
415
     *
416
     * @param \ReflectionClass $reflectionClass
417
     *
418
     * @return array
419
     */
420
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
421
    {
422
        if (in_array($reflectionClass->getName(), $this->properties)) {
423
            return $this->properties[$reflectionClass->getName()];
424
        }
425
426
        $properties = [];
427
428
        foreach ($reflectionClass->getProperties() as $property) {
429
            if (!in_array($property->getName(), $properties)) {
430
                $properties[$property->getName()] = $property;
431
            }
432
        }
433
434
        $parentReflection = $reflectionClass->getParentClass();
435
        if ($parentReflection !== false) {
436
            $properties = array_merge(
437
                $properties,
438
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
439
            );
440
        }
441
442
        $this->properties[$reflectionClass->getName()] = $properties;
443
444
        return $properties;
445
    }
446
447
    /**
448
     * Parses analyzers list from document mapping.
449
     *
450
     * @param \ReflectionClass $reflectionClass
451
     * @return array
452
     */
453
    private function getAnalyzers(\ReflectionClass $reflectionClass)
454
    {
455
        $analyzers = [];
456
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
457
            $type = $this->getPropertyAnnotationData($property);
458
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
459
460
            if ($type instanceof Embedded) {
461
                $analyzers = array_merge(
462
                    $analyzers,
463
                    $this->getAnalyzers(new \ReflectionClass($this->finder->getNamespace($type->class)))
464
                );
465
            }
466
467
            if ($type instanceof Property) {
468
                if (isset($type->options['analyzer'])) {
469
                    $analyzers[] = $type->options['analyzer'];
470
                }
471
                if (isset($type->options['search_analyzer'])) {
472
                    $analyzers[] = $type->options['search_analyzer'];
473
                }
474
475
                if (isset($type->options['fields'])) {
476
                    foreach ($type->options['fields'] as $field) {
477
                        if (isset($field['analyzer'])) {
478
                            $analyzers[] = $field['analyzer'];
479
                        }
480
                        if (isset($field['search_analyzer'])) {
481
                            $analyzers[] = $field['search_analyzer'];
482
                        }
483
                    }
484
                }
485
            }
486
        }
487
        return array_unique($analyzers);
488
    }
489
490
    /**
491
     * Returns properties of reflection class.
492
     *
493
     * @param \ReflectionClass $reflectionClass Class to read properties from.
494
     * @param array            $properties      Properties to skip.
495
     * @param bool             $flag            If false exludes properties, true only includes properties.
496
     *
497
     * @return array
498
     */
499
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
500
    {
501
        $mapping = [];
502
        /** @var \ReflectionProperty $property */
503
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
504
            $type = $this->getPropertyAnnotationData($property);
505
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
506
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
507
508
            if ((in_array($name, $properties) && !$flag)
509
                || (!in_array($name, $properties) && $flag)
510
                || empty($type)
511
            ) {
512
                continue;
513
            }
514
515
            $map = $type->dump();
516
517
            // Inner object
518
            if ($type instanceof Embedded) {
519
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class));
520
            }
521
522
            // HashMap object
523
            if ($type instanceof HashMap) {
524
                $map = array_replace_recursive($map, [
525
                    'type' => Nested::NAME,
526
                    'dynamic' => true,
527
                ]);
528
            }
529
530
            // If there is set some Raw options, it will override current ones.
531
            if (isset($map['options'])) {
532
                $options = $map['options'];
533
                unset($map['options']);
534
                $map = array_merge($map, $options);
535
            }
536
537
            $mapping[$type->name] = $map;
538
        }
539
540
        return $mapping;
541
    }
542
543
    /**
544
     * Returns object mapping.
545
     *
546
     * Loads from cache if it's already loaded.
547
     *
548
     * @param string $className
549
     *
550
     * @return array
551
     */
552
    private function getObjectMapping($className)
553
    {
554
        $namespace = $this->finder->getNamespace($className);
555
556
        if (array_key_exists($namespace, $this->objects)) {
557
            return $this->objects[$namespace];
558
        }
559
560
        $reflectionClass = new \ReflectionClass($namespace);
561
562
        switch (true) {
563
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
564
                $type = 'object';
565
                break;
566
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
567
                $type = 'nested';
568
                break;
569
            default:
570
                throw new \LogicException(
571
                    sprintf(
572
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
573
                        $className
574
                    )
575
                );
576
        }
577
578
        $this->objects[$namespace] = [
579
            'type' => $type,
580
            'properties' => $this->getProperties($reflectionClass),
581
        ];
582
583
        return $this->objects[$namespace];
584
    }
585
}
586