Completed
Pull Request — master (#537)
by Mantas
27:04 queued 01:46
created

DocumentParser::getDocumentAnnotationData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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