Completed
Pull Request — master (#523)
by Mantas
03:09
created

DocumentParser::getPropertyAnnotationData()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

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