Completed
Pull Request — master (#509)
by Mantas
02:28
created

DocumentParser::getDocumentAnnotationData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

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