Completed
Pull Request — 5.0 (#804)
by Alexander
06:14
created

DocumentParser::guessDirName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 1
eloc 5
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\HashMap;
19
use ONGR\ElasticsearchBundle\Annotation\MetaField;
20
use ONGR\ElasticsearchBundle\Annotation\Nested;
21
use ONGR\ElasticsearchBundle\Annotation\ParentDocument;
22
use ONGR\ElasticsearchBundle\Annotation\Property;
23
use ONGR\ElasticsearchBundle\Exception\MissingDocumentAnnotationException;
24
25
/**
26
 * Document parser used for reading document annotations.
27
 */
28
class DocumentParser
29
{
30
    const PROPERTY_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Property';
31
    const EMBEDDED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Embedded';
32
    const DOCUMENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Document';
33
    const OBJECT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Object';
34
    const NESTED_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Nested';
35
36
    // Meta fields
37
    const ID_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Id';
38
    const PARENT_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\ParentDocument';
39
    const ROUTING_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Routing';
40
    const VERSION_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\Version';
41
    const HASH_MAP_ANNOTATION = 'ONGR\ElasticsearchBundle\Annotation\HashMap';
42
43
    /**
44
     * @var Reader Used to read document annotations.
45
     */
46
    private $reader;
47
48
    /**
49
     * @var DocumentFinder Used to find documents.
50
     */
51
    private $finder;
52
53
    /**
54
     * @var array Contains gathered objects which later adds to documents.
55
     */
56
    private $objects = [];
57
58
    /**
59
     * @var array Document properties aliases.
60
     */
61
    private $aliases = [];
62
63
    /**
64
     * @var array Local cache for document properties.
65
     */
66
    private $properties = [];
67
68
    /**
69
     * @var array Local cache for documents
70
     */
71
    private $documents = [];
72
73
    /**
74
     * @param Reader         $reader Used for reading annotations.
75
     * @param DocumentFinder $finder Used for resolving namespaces.
76
     */
77
    public function __construct(Reader $reader, DocumentFinder $finder)
78
    {
79
        $this->reader = $reader;
80
        $this->finder = $finder;
81
        $this->registerAnnotations();
82
    }
83
84
    /**
85
     * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata.
86
     *
87
     * @param \ReflectionClass $class
88
     *
89
     * @return array|null
90
     * @throws MissingDocumentAnnotationException
91
     */
92
    public function parse(\ReflectionClass $class)
93
    {
94
        $className = $class->getName();
95
96
        if ($class->isTrait()) {
97
            return false;
98
        }
99
100
        if (!isset($this->documents[$className])) {
101
            /** @var Document $document */
102
            $document = $this->reader->getClassAnnotation($class, self::DOCUMENT_ANNOTATION);
103
104
            if ($document === null) {
105
                throw new MissingDocumentAnnotationException(
106
                    sprintf(
107
                        '"%s" class cannot be parsed as document because @Document annotation is missing.',
108
                        $class->getName()
109
                    )
110
                );
111
            }
112
113
            $fields = [];
114
115
            $this->documents[$className] = [
116
                'type' => $document->type ?: Caser::snake($class->getShortName()),
117
                'properties' => $this->getProperties($class),
118
                'fields' => array_filter(
119
                    array_merge(
120
                        $document->dump(),
121
                        $fields
122
                    )
123
                ),
124
                'aliases' => $this->getAliases($class, $fields),
125
                'analyzers' => $this->getAnalyzers($class),
126
                'objects' => $this->getObjects(),
127
                'namespace' => $class->getName(),
128
                'class' => $class->getShortName(),
129
            ];
130
        }
131
        return $this->documents[$className];
132
    }
133
134
    /**
135
     * Returns document annotation data from reader.
136
     *
137
     * @param \ReflectionClass $document
138
     *
139
     * @return Document|object|null
140
     */
141
    private function getDocumentAnnotationData($document)
142
    {
143
        return $this->reader->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
144
    }
145
146
    /**
147
     * Returns property annotation data from reader.
148
     *
149
     * @param \ReflectionProperty $property
150
     *
151
     * @return Property|object|null
152
     */
153 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...
154
    {
155
        $result = $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION);
156
157
        if ($result !== null && $result->name === null) {
158
            $result->name = Caser::snake($property->getName());
159
        }
160
161
        return $result;
162
    }
163
164
    /**
165
     * Returns Embedded annotation data from reader.
166
     *
167
     * @param \ReflectionProperty $property
168
     *
169
     * @return Embedded|object|null
170
     */
171 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...
172
    {
173
        $result = $this->reader->getPropertyAnnotation($property, self::EMBEDDED_ANNOTATION);
174
175
        if ($result !== null && $result->name === null) {
176
            $result->name = Caser::snake($property->getName());
177
        }
178
179
        return $result;
180
    }
181
182
    /**
183
     * Returns HashMap annotation data from reader.
184
     *
185
     * @param \ReflectionProperty $property
186
     *
187
     * @return HashMap|object|null
188
     */
189 View Code Duplication
    private function getHashMapAnnotationData(\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...
190
    {
191
        $result = $this->reader->getPropertyAnnotation($property, self::HASH_MAP_ANNOTATION);
192
193
        if ($result !== null && $result->name === null) {
194
            $result->name = Caser::snake($property->getName());
195
        }
196
197
        return $result;
198
    }
199
200
    /**
201
     * Returns meta field annotation data from reader.
202
     *
203
     * @param \ReflectionProperty $property
204
     *
205
     * @return array
206
     */
207
    private function getMetaFieldAnnotationData($property)
208
    {
209
        /** @var MetaField $annotation */
210
        $annotation = $this->reader->getPropertyAnnotation($property, self::ID_ANNOTATION);
211
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::PARENT_ANNOTATION);
212
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::ROUTING_ANNOTATION);
213
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::VERSION_ANNOTATION);
214
215
        if ($annotation === null) {
216
            return null;
217
        }
218
219
        $data = [
220
            'name' => $annotation->getName(),
221
            'settings' => $annotation->getSettings(),
222
        ];
223
224
        if ($annotation instanceof ParentDocument) {
225
            $data['settings']['type'] = $this->getDocumentType($annotation->class);
226
        }
227
228
        return $data;
229
    }
230
231
    /**
232
     * Returns objects used in document.
233
     *
234
     * @return array
235
     */
236
    private function getObjects()
237
    {
238
        return array_keys($this->objects);
239
    }
240
241
    /**
242
     * Finds aliases for every property used in document including parent classes.
243
     *
244
     * @param \ReflectionClass $reflectionClass
245
     * @param array            $metaFields
246
     *
247
     * @return array
248
     */
249
    private function getAliases(\ReflectionClass $reflectionClass, array &$metaFields = null)
250
    {
251
        $reflectionName = $reflectionClass->getName();
252
253
        // We skip cache in case $metaFields is given. This should not affect performance
254
        // because for each document this method is called only once. For objects it might
255
        // be called few times.
256
        if ($metaFields === null && array_key_exists($reflectionName, $this->aliases)) {
257
            return $this->aliases[$reflectionName];
258
        }
259
260
        $alias = [];
261
262
        /** @var \ReflectionProperty[] $properties */
263
        $properties = $this->getDocumentPropertiesReflection($reflectionClass);
264
265
        foreach ($properties as $name => $property) {
266
            $type = $this->getPropertyAnnotationData($property);
267
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
268
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
269
270
            if ($type === null && $metaFields !== null
271
                && ($metaData = $this->getMetaFieldAnnotationData($property)) !== null) {
272
                $metaFields[$metaData['name']] = $metaData['settings'];
273
                $type = new \stdClass();
274
                $type->name = $metaData['name'];
275
            }
276
            if ($type !== null) {
277
                $alias[$type->name] = [
278
                    'propertyName' => $name,
279
                ];
280
281
                if ($type instanceof Property) {
282
                    $alias[$type->name]['type'] = $type->type;
283
                }
284
285
                if ($type instanceof HashMap) {
286
                    $alias[$type->name]['type'] = HashMap::NAME;
287
                }
288
289
                $alias[$type->name][HashMap::NAME] = $type instanceof HashMap;
290
291
                switch (true) {
292
                    case $property->isPublic():
293
                        $propertyType = 'public';
294
                        break;
295
                    case $property->isProtected():
296
                    case $property->isPrivate():
297
                        $propertyType = 'private';
298
                        $alias[$type->name]['methods'] = $this->getMutatorMethods(
299
                            $reflectionClass,
300
                            $name,
301
                            $type instanceof Property ? $type->type : null
302
                        );
303
                        break;
304
                    default:
305
                        $message = sprintf(
306
                            'Wrong property %s type of %s class types cannot '.
307
                            'be static or abstract.',
308
                            $name,
309
                            $reflectionName
310
                        );
311
                        throw new \LogicException($message);
312
                }
313
                $alias[$type->name]['propertyType'] = $propertyType;
314
315
                if ($type instanceof Embedded) {
316
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class));
317
                    $alias[$type->name] = array_merge(
318
                        $alias[$type->name],
319
                        [
320
                            'type' => $this->getObjectMapping($type->class)['type'],
321
                            'multiple' => $type->multiple,
322
                            'aliases' => $this->getAliases($child, $metaFields),
323
                            'namespace' => $child->getName(),
324
                        ]
325
                    );
326
                }
327
            }
328
        }
329
330
        $this->aliases[$reflectionName] = $alias;
331
332
        return $this->aliases[$reflectionName];
333
    }
334
335
    /**
336
     * Checks if class have setter and getter, and returns them in array.
337
     *
338
     * @param \ReflectionClass $reflectionClass
339
     * @param string           $property
340
     *
341
     * @return array
342
     */
343
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
344
    {
345
        $camelCaseName = ucfirst(Caser::camel($property));
346
        $setterName = 'set'.$camelCaseName;
347
        if (!$reflectionClass->hasMethod($setterName)) {
348
            $message = sprintf(
349
                'Missing %s() method in %s class. Add it, or change property to public.',
350
                $setterName,
351
                $reflectionClass->getName()
352
            );
353
            throw new \LogicException($message);
354
        }
355
356 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...
357
            return [
358
                'getter' => 'get' . $camelCaseName,
359
                'setter' => $setterName
360
            ];
361
        }
362
363
        if ($propertyType === 'boolean') {
364 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...
365
                return [
366
                    'getter' => 'is' . $camelCaseName,
367
                    'setter' => $setterName
368
                ];
369
            }
370
371
            $message = sprintf(
372
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
373
                'get'.$camelCaseName,
374
                'is'.$camelCaseName,
375
                $reflectionClass->getName()
376
            );
377
            throw new \LogicException($message);
378
        }
379
380
        $message = sprintf(
381
            'Missing %s() method in %s class. Add it, or change property to public.',
382
            'get'.$camelCaseName,
383
            $reflectionClass->getName()
384
        );
385
        throw new \LogicException($message);
386
    }
387
388
    /**
389
     * Registers annotations to registry so that it could be used by reader.
390
     */
391
    private function registerAnnotations()
392
    {
393
        $annotations = [
394
            'Document',
395
            'Property',
396
            'Embedded',
397
            'Object',
398
            'Nested',
399
            'Id',
400
            'ParentDocument',
401
            'Routing',
402
            'Version',
403
            'HashMap',
404
        ];
405
406
        foreach ($annotations as $annotation) {
407
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...egistry::registerFile() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
408
        }
409
    }
410
411
    /**
412
     * Returns document type.
413
     *
414
     * @param string $document  Format must be like AcmeBundle:Document.
415
     *
416
     * @return string
417
     */
418
    private function getDocumentType($document)
419
    {
420
        $namespace = $this->finder->getNamespace($document);
421
        $reflectionClass = new \ReflectionClass($namespace);
422
        $document = $this->getDocumentAnnotationData($reflectionClass);
423
424
        return empty($document->type) ? Caser::snake($reflectionClass->getShortName()) : $document->type;
425
    }
426
427
    /**
428
     * Returns all defined properties including private from parents.
429
     *
430
     * @param \ReflectionClass $reflectionClass
431
     *
432
     * @return array
433
     */
434
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
435
    {
436
        if (in_array($reflectionClass->getName(), $this->properties)) {
437
            return $this->properties[$reflectionClass->getName()];
438
        }
439
440
        $properties = [];
441
442
        foreach ($reflectionClass->getProperties() as $property) {
443
            if (!in_array($property->getName(), $properties)) {
444
                $properties[$property->getName()] = $property;
445
            }
446
        }
447
448
        $parentReflection = $reflectionClass->getParentClass();
449
        if ($parentReflection !== false) {
450
            $properties = array_merge(
451
                $properties,
452
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
453
            );
454
        }
455
456
        $this->properties[$reflectionClass->getName()] = $properties;
457
458
        return $properties;
459
    }
460
461
    /**
462
     * Parses analyzers list from document mapping.
463
     *
464
     * @param \ReflectionClass $reflectionClass
465
     * @return array
466
     */
467
    private function getAnalyzers(\ReflectionClass $reflectionClass)
468
    {
469
        $analyzers = [];
470
471
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
472
            $type = $this->getPropertyAnnotationData($property);
473
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
474
475
            if ($type instanceof Embedded) {
476
                $analyzers = array_merge(
477
                    $analyzers,
478
                    $this->getAnalyzers(new \ReflectionClass($this->finder->getNamespace($type->class)))
479
                );
480
            }
481
482
            if ($type instanceof Property) {
483
                if (isset($type->options['analyzer'])) {
484
                    $analyzers[] = $type->options['analyzer'];
485
                }
486
                if (isset($type->options['search_analyzer'])) {
487
                    $analyzers[] = $type->options['search_analyzer'];
488
                }
489
490
                if (isset($type->options['fields'])) {
491
                    foreach ($type->options['fields'] as $field) {
492
                        if (isset($field['analyzer'])) {
493
                            $analyzers[] = $field['analyzer'];
494
                        }
495
                        if (isset($field['search_analyzer'])) {
496
                            $analyzers[] = $field['search_analyzer'];
497
                        }
498
                    }
499
                }
500
            }
501
        }
502
        return array_unique($analyzers);
503
    }
504
505
    /**
506
     * Returns properties of reflection class.
507
     *
508
     * @param \ReflectionClass $reflectionClass Class to read properties from.
509
     * @param array            $properties      Properties to skip.
510
     * @param bool             $flag            If false exludes properties, true only includes properties.
511
     *
512
     * @return array
513
     */
514
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
515
    {
516
        $mapping = [];
517
518
        /** @var \ReflectionProperty $property */
519
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
520
            $type = $this->getPropertyAnnotationData($property);
521
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
522
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
523
524
            if ((in_array($name, $properties) && !$flag)
525
                || (!in_array($name, $properties) && $flag)
526
                || empty($type)
527
            ) {
528
                continue;
529
            }
530
531
            $map = $type->dump();
532
533
            // Inner object
534
            if ($type instanceof Embedded) {
535
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class));
536
            }
537
538
            // HashMap object
539
            if ($type instanceof HashMap) {
540
                $map = array_replace_recursive($map, [
541
                    'type' => Nested::NAME,
542
                    'dynamic' => true,
543
                ]);
544
            }
545
546
            // If there is set some Raw options, it will override current ones.
547
            if (isset($map['options'])) {
548
                $options = $map['options'];
549
                unset($map['options']);
550
                $map = array_merge($map, $options);
551
            }
552
553
            $mapping[$type->name] = $map;
554
        }
555
556
        return $mapping;
557
    }
558
559
    /**
560
     * Returns object mapping.
561
     *
562
     * Loads from cache if it's already loaded.
563
     *
564
     * @param string $className
565
     *
566
     * @return array
567
     */
568
    private function getObjectMapping($className)
569
    {
570
        $namespace = $this->finder->getNamespace($className);
571
572
        if (array_key_exists($namespace, $this->objects)) {
573
            return $this->objects[$namespace];
574
        }
575
576
        $reflectionClass = new \ReflectionClass($namespace);
577
578
        switch (true) {
579
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
580
                $type = 'object';
581
                break;
582
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
583
                $type = 'nested';
584
                break;
585
            default:
586
                throw new \LogicException(
587
                    sprintf(
588
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
589
                        $className
590
                    )
591
                );
592
        }
593
594
        $this->objects[$namespace] = [
595
            'type' => $type,
596
            'properties' => $this->getProperties($reflectionClass),
597
        ];
598
599
        return $this->objects[$namespace];
600
    }
601
}
602