Completed
Push — master ( 2578eb...fd195b )
by Simonas
06:23 queued 03:49
created

DocumentParser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4286
cc 1
eloc 4
nc 1
nop 2
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(
227
                            $reflectionClass,
228
                            $name,
229
                            $type instanceof Property ? $type->type : null
230
                        );
231
                        break;
232
                    default:
233
                        $message = sprintf(
234
                            'Wrong property %s type of %s class types cannot '.
235
                            'be static or abstract.',
236
                            $name,
237
                            $reflectionName
238
                        );
239
                        throw new \LogicException($message);
240
                }
241
                $alias[$type->name]['propertyType'] = $propertyType;
242
243
                if ($type instanceof Embedded) {
244
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class));
245
                    $alias[$type->name] = array_merge(
246
                        $alias[$type->name],
247
                        [
248
                            'type' => $this->getObjectMapping($type->class)['type'],
249
                            'multiple' => $type->multiple,
250
                            'aliases' => $this->getAliases($child),
251
                            'namespace' => $child->getName(),
252
                        ]
253
                    );
254
                }
255
            }
256
        }
257
258
        $this->aliases[$reflectionName] = $alias;
259
260
        return $this->aliases[$reflectionName];
261
    }
262
263
    /**
264
     * Checks if class have setter and getter, and returns them in array.
265
     *
266
     * @param \ReflectionClass $reflectionClass
267
     * @param string           $property
268
     *
269
     * @return array
270
     */
271
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
272
    {
273
        $camelCaseName = ucfirst(Caser::camel($property));
274
        $setterName = 'set'.$camelCaseName;
275
        if (!$reflectionClass->hasMethod($setterName)) {
276
            $message = sprintf(
277
                'Missing %s() method in %s class. Add it, or change property to public.',
278
                $setterName,
279
                $reflectionClass->getName()
280
            );
281
            throw new \LogicException($message);
282
        }
283
284 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...
285
            return [
286
                'getter' => 'get' . $camelCaseName,
287
                'setter' => $setterName
288
            ];
289
        }
290
291
        if ($propertyType === 'boolean') {
292 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...
293
                return [
294
                    'getter' => 'is' . $camelCaseName,
295
                    'setter' => $setterName
296
                ];
297
            }
298
299
            $message = sprintf(
300
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
301
                'get'.$camelCaseName,
302
                'is'.$camelCaseName,
303
                $reflectionClass->getName()
304
            );
305
            throw new \LogicException($message);
306
        }
307
308
        $message = sprintf(
309
            'Missing %s() method in %s class. Add it, or change property to public.',
310
            'get'.$camelCaseName,
311
            $reflectionClass->getName()
312
        );
313
        throw new \LogicException($message);
314
    }
315
316
    /**
317
     * Registers annotations to registry so that it could be used by reader.
318
     */
319
    private function registerAnnotations()
320
    {
321
        $annotations = [
322
            'Document',
323
            'MetaField',
324
            'Property',
325
            'Embedded',
326
            'Object',
327
            'Nested',
328
        ];
329
330
        foreach ($annotations as $annotation) {
331
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
332
        }
333
    }
334
335
    /**
336
     * Returns document type.
337
     *
338
     * @param string $document Format must be like AcmeBundle:Document.
339
     *
340
     * @return string
341
     */
342
    private function getDocumentType($document)
343
    {
344
        $namespace = $this->finder->getNamespace($document);
345
        $reflectionClass = new \ReflectionClass($namespace);
346
        $document = $this->getDocumentAnnotationData($reflectionClass);
347
348
        return empty($document->type) ? $reflectionClass->getShortName() : $document->type;
349
    }
350
351
    /**
352
     * Returns all defined properties including private from parents.
353
     *
354
     * @param \ReflectionClass $reflectionClass
355
     *
356
     * @return array
357
     */
358
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
359
    {
360
        if (in_array($reflectionClass->getName(), $this->properties)) {
361
            return $this->properties[$reflectionClass->getName()];
362
        }
363
364
        $properties = [];
365
366
        foreach ($reflectionClass->getProperties() as $property) {
367
            if (!in_array($property->getName(), $properties)) {
368
                $properties[$property->getName()] = $property;
369
            }
370
        }
371
372
        $parentReflection = $reflectionClass->getParentClass();
373
        if ($parentReflection !== false) {
374
            $properties = array_merge(
375
                $properties,
376
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
377
            );
378
        }
379
380
        $this->properties[$reflectionClass->getName()] = $properties;
381
382
        return $properties;
383
    }
384
385
    /**
386
     * Returns properties of reflection class.
387
     *
388
     * @param \ReflectionClass $reflectionClass Class to read properties from.
389
     * @param array            $properties      Properties to skip.
390
     * @param bool             $flag            If false exludes properties, true only includes properties.
391
     *
392
     * @return array
393
     */
394
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
395
    {
396
        $mapping = [];
397
        /** @var \ReflectionProperty $property */
398
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
399
            $type = $this->getPropertyAnnotationData($property);
400
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
401
402
            if ((in_array($name, $properties) && !$flag)
403
                || (!in_array($name, $properties) && $flag)
404
                || empty($type)
405
            ) {
406
                continue;
407
            }
408
409
            $map = $type->dump();
410
411
            // Inner object
412
            if ($type instanceof Embedded) {
413
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class));
414
            }
415
416
            // If there is set some Raw options, it will override current ones.
417
            if (isset($map['options'])) {
418
                $options = $map['options'];
419
                unset($map['options']);
420
                $map = array_merge($map, $options);
421
            }
422
423
            $mapping[$type->name] = $map;
424
        }
425
426
        return $mapping;
427
    }
428
429
    /**
430
     * Returns object mapping.
431
     *
432
     * Loads from cache if it's already loaded.
433
     *
434
     * @param string $className
435
     *
436
     * @return array
437
     */
438
    private function getObjectMapping($className)
439
    {
440
        $namespace = $this->finder->getNamespace($className);
441
442
        if (array_key_exists($namespace, $this->objects)) {
443
            return $this->objects[$namespace];
444
        }
445
446
        $reflectionClass = new \ReflectionClass($namespace);
447
448
        switch (true) {
449
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
450
                $type = 'object';
451
                break;
452
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
453
                $type = 'nested';
454
                break;
455
            default:
456
                throw new \LogicException(
457
                    sprintf(
458
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
459
                        $className
460
                    )
461
                );
462
        }
463
464
        $this->objects[$namespace] = [
465
            'type' => $type,
466
            'properties' => $this->getProperties($reflectionClass),
467
        ];
468
469
        return $this->objects[$namespace];
470
    }
471
}
472