Completed
Push — master ( bee9de...97e386 )
by Simonas
16:39 queued 01:18
created

DocumentParser::getMetaFieldAnnotationData()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
cc 6
eloc 13
nc 24
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\MetaField;
19
use ONGR\ElasticsearchBundle\Annotation\ParentDocument;
20
use ONGR\ElasticsearchBundle\Annotation\Property;
21
use ONGR\ElasticsearchBundle\Exception\MissingDocumentAnnotationException;
22
23
/**
24
 * Document parser used for reading document annotations.
25
 */
26
class DocumentParser
27
{
28
    const PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Property';
29
    const EMBEDDED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Embedded';
30
    const DOCUMENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Document';
31
    const OBJECT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Object';
32
    const NESTED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Nested';
33
34
    // Meta fields
35
    const ID_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Id';
36
    const PARENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\ParentDocument';
37
    const ROUTING_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Routing';
38
    const VERSION_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Version';
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
     * Analyzers used in documents.
67
     *
68
     * @var string[]
69
     */
70
    private $analyzers = [];
0 ignored issues
show
Unused Code introduced by
The property $analyzers is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

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