Completed
Push — master ( 18b9a5...070941 )
by Simonas
02:16
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
     * @param Reader         $reader Used for reading annotations.
70
     * @param DocumentFinder $finder Used for resolving namespaces.
71
     */
72
    public function __construct(Reader $reader, DocumentFinder $finder)
73
    {
74
        $this->reader = $reader;
75
        $this->finder = $finder;
76
        $this->registerAnnotations();
77
    }
78
79
    /**
80
     * Parses documents by used annotations and returns mapping for elasticsearch with some extra metadata.
81
     *
82
     * @param \ReflectionClass $class
83
     *
84
     * @return array
85
     * @throws MissingDocumentAnnotationException
86
     */
87
    public function parse(\ReflectionClass $class)
88
    {
89
        /** @var Document $document */
90
        $document = $this->reader->getClassAnnotation($class, self::DOCUMENT_ANNOTATION);
91
92
        if ($document === null) {
93
            throw new MissingDocumentAnnotationException(
94
                sprintf(
95
                    '"%s" class cannot be parsed as document because @Document annotation is missing.',
96
                    $class->getName()
97
                )
98
            );
99
        }
100
101
        $fields = [];
102
103
        return [
104
            'type' => $document->type ?: Caser::snake($class->getShortName()),
105
            'properties' => $this->getProperties($class),
106
            'fields' => array_filter(
107
                array_merge(
108
                    $document->dump(),
109
                    $fields
110
                )
111
            ),
112
            'aliases' => $this->getAliases($class, $fields),
113
            'analyzers' => $this->getAnalyzers($class),
114
            'objects' => $this->getObjects(),
115
            'namespace' => $class->getName(),
116
            'class' => $class->getShortName(),
117
        ];
118
    }
119
120
    /**
121
     * Returns document annotation data from reader.
122
     *
123
     * @param \ReflectionClass $document
124
     *
125
     * @return Document|object|null
126
     */
127
    private function getDocumentAnnotationData($document)
128
    {
129
        return $this->reader->getClassAnnotation($document, self::DOCUMENT_ANNOTATION);
130
    }
131
132
    /**
133
     * Returns property annotation data from reader.
134
     *
135
     * @param \ReflectionProperty $property
136
     *
137
     * @return Property|object|null
138
     */
139 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...
140
    {
141
        $result = $this->reader->getPropertyAnnotation($property, self::PROPERTY_ANNOTATION);
142
143
        if ($result !== null && $result->name === null) {
144
            $result->name = Caser::snake($property->getName());
145
        }
146
147
        return $result;
148
    }
149
150
    /**
151
     * Returns Embedded annotation data from reader.
152
     *
153
     * @param \ReflectionProperty $property
154
     *
155
     * @return Embedded|object|null
156
     */
157 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...
158
    {
159
        $result = $this->reader->getPropertyAnnotation($property, self::EMBEDDED_ANNOTATION);
160
161
        if ($result !== null && $result->name === null) {
162
            $result->name = Caser::snake($property->getName());
163
        }
164
165
        return $result;
166
    }
167
168
    /**
169
     * Returns HashMap annotation data from reader.
170
     *
171
     * @param \ReflectionProperty $property
172
     *
173
     * @return HashMap|object|null
174
     */
175 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...
176
    {
177
        $result = $this->reader->getPropertyAnnotation($property, self::HASH_MAP_ANNOTATION);
178
179
        if ($result !== null && $result->name === null) {
180
            $result->name = Caser::snake($property->getName());
181
        }
182
183
        return $result;
184
    }
185
186
    /**
187
     * Returns meta field annotation data from reader.
188
     *
189
     * @param \ReflectionProperty $property
190
     * @param string              $directory The name of the Document directory in the bundle
191
     *
192
     * @return array
193
     */
194
    private function getMetaFieldAnnotationData($property, $directory)
195
    {
196
        /** @var MetaField $annotation */
197
        $annotation = $this->reader->getPropertyAnnotation($property, self::ID_ANNOTATION);
198
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::PARENT_ANNOTATION);
199
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::ROUTING_ANNOTATION);
200
        $annotation = $annotation ?: $this->reader->getPropertyAnnotation($property, self::VERSION_ANNOTATION);
201
202
        if ($annotation === null) {
203
            return null;
204
        }
205
206
        $data = [
207
            'name' => $annotation->getName(),
208
            'settings' => $annotation->getSettings(),
209
        ];
210
211
        if ($annotation instanceof ParentDocument) {
212
            $data['settings']['type'] = $this->getDocumentType($annotation->class, $directory);
213
        }
214
215
        return $data;
216
    }
217
218
    /**
219
     * Returns objects used in document.
220
     *
221
     * @return array
222
     */
223
    private function getObjects()
224
    {
225
        return array_keys($this->objects);
226
    }
227
228
    /**
229
     * Finds aliases for every property used in document including parent classes.
230
     *
231
     * @param \ReflectionClass $reflectionClass
232
     * @param array            $metaFields
233
     *
234
     * @return array
235
     */
236
    private function getAliases(\ReflectionClass $reflectionClass, array &$metaFields = null)
237
    {
238
        $reflectionName = $reflectionClass->getName();
239
        $directory = $this->guessDirName($reflectionClass);
240
241
        // We skip cache in case $metaFields is given. This should not affect performance
242
        // because for each document this method is called only once. For objects it might
243
        // be called few times.
244
        if ($metaFields === null && array_key_exists($reflectionName, $this->aliases)) {
245
            return $this->aliases[$reflectionName];
246
        }
247
248
        $alias = [];
249
250
        /** @var \ReflectionProperty[] $properties */
251
        $properties = $this->getDocumentPropertiesReflection($reflectionClass);
252
253
        foreach ($properties as $name => $property) {
254
            $type = $this->getPropertyAnnotationData($property);
255
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
256
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
257
258
            if ($type === null && $metaFields !== null
259
                && ($metaData = $this->getMetaFieldAnnotationData($property, $directory)) !== null) {
260
                $metaFields[$metaData['name']] = $metaData['settings'];
261
                $type = new \stdClass();
262
                $type->name = $metaData['name'];
263
            }
264
            if ($type !== null) {
265
                $alias[$type->name] = [
266
                    'propertyName' => $name,
267
                ];
268
269
                if ($type instanceof Property) {
270
                    $alias[$type->name]['type'] = $type->type;
271
                }
272
273
                if ($type instanceof HashMap) {
274
                    $alias[$type->name]['type'] = HashMap::NAME;
275
                }
276
277
                $alias[$type->name][HashMap::NAME] = $type instanceof HashMap;
278
279
                switch (true) {
280
                    case $property->isPublic():
281
                        $propertyType = 'public';
282
                        break;
283
                    case $property->isProtected():
284
                    case $property->isPrivate():
285
                        $propertyType = 'private';
286
                        $alias[$type->name]['methods'] = $this->getMutatorMethods(
287
                            $reflectionClass,
288
                            $name,
289
                            $type instanceof Property ? $type->type : null
290
                        );
291
                        break;
292
                    default:
293
                        $message = sprintf(
294
                            'Wrong property %s type of %s class types cannot '.
295
                            'be static or abstract.',
296
                            $name,
297
                            $reflectionName
298
                        );
299
                        throw new \LogicException($message);
300
                }
301
                $alias[$type->name]['propertyType'] = $propertyType;
302
303
                if ($type instanceof Embedded) {
304
                    $child = new \ReflectionClass($this->finder->getNamespace($type->class, $directory));
305
                    $alias[$type->name] = array_merge(
306
                        $alias[$type->name],
307
                        [
308
                            'type' => $this->getObjectMapping($type->class, $directory)['type'],
309
                            'multiple' => $type->multiple,
310
                            'aliases' => $this->getAliases($child, $metaFields),
311
                            'namespace' => $child->getName(),
312
                        ]
313
                    );
314
                }
315
            }
316
        }
317
318
        $this->aliases[$reflectionName] = $alias;
319
320
        return $this->aliases[$reflectionName];
321
    }
322
323
    /**
324
     * Checks if class have setter and getter, and returns them in array.
325
     *
326
     * @param \ReflectionClass $reflectionClass
327
     * @param string           $property
328
     *
329
     * @return array
330
     */
331
    private function getMutatorMethods(\ReflectionClass $reflectionClass, $property, $propertyType)
332
    {
333
        $camelCaseName = ucfirst(Caser::camel($property));
334
        $setterName = 'set'.$camelCaseName;
335
        if (!$reflectionClass->hasMethod($setterName)) {
336
            $message = sprintf(
337
                'Missing %s() method in %s class. Add it, or change property to public.',
338
                $setterName,
339
                $reflectionClass->getName()
340
            );
341
            throw new \LogicException($message);
342
        }
343
344 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...
345
            return [
346
                'getter' => 'get' . $camelCaseName,
347
                'setter' => $setterName
348
            ];
349
        }
350
351
        if ($propertyType === 'boolean') {
352 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...
353
                return [
354
                    'getter' => 'is' . $camelCaseName,
355
                    'setter' => $setterName
356
                ];
357
            }
358
359
            $message = sprintf(
360
                'Missing %s() or %s() method in %s class. Add it, or change property to public.',
361
                'get'.$camelCaseName,
362
                'is'.$camelCaseName,
363
                $reflectionClass->getName()
364
            );
365
            throw new \LogicException($message);
366
        }
367
368
        $message = sprintf(
369
            'Missing %s() method in %s class. Add it, or change property to public.',
370
            'get'.$camelCaseName,
371
            $reflectionClass->getName()
372
        );
373
        throw new \LogicException($message);
374
    }
375
376
    /**
377
     * Registers annotations to registry so that it could be used by reader.
378
     */
379
    private function registerAnnotations()
380
    {
381
        $annotations = [
382
            'Document',
383
            'Property',
384
            'Embedded',
385
            'Object',
386
            'Nested',
387
            'Id',
388
            'ParentDocument',
389
            'Routing',
390
            'Version',
391
            'HashMap',
392
        ];
393
394
        foreach ($annotations as $annotation) {
395
            AnnotationRegistry::registerFile(__DIR__ . "/../Annotation/{$annotation}.php");
396
        }
397
    }
398
399
    /**
400
     * Returns document type.
401
     *
402
     * @param string $document  Format must be like AcmeBundle:Document.
403
     * @param string $directory The Document directory name of the bundle.
404
     *
405
     * @return string
406
     */
407
    private function getDocumentType($document, $directory)
408
    {
409
        $namespace = $this->finder->getNamespace($document, $directory);
410
        $reflectionClass = new \ReflectionClass($namespace);
411
        $document = $this->getDocumentAnnotationData($reflectionClass);
412
413
        return empty($document->type) ? Caser::snake($reflectionClass->getShortName()) : $document->type;
414
    }
415
416
    /**
417
     * Returns all defined properties including private from parents.
418
     *
419
     * @param \ReflectionClass $reflectionClass
420
     *
421
     * @return array
422
     */
423
    private function getDocumentPropertiesReflection(\ReflectionClass $reflectionClass)
424
    {
425
        if (in_array($reflectionClass->getName(), $this->properties)) {
426
            return $this->properties[$reflectionClass->getName()];
427
        }
428
429
        $properties = [];
430
431
        foreach ($reflectionClass->getProperties() as $property) {
432
            if (!in_array($property->getName(), $properties)) {
433
                $properties[$property->getName()] = $property;
434
            }
435
        }
436
437
        $parentReflection = $reflectionClass->getParentClass();
438
        if ($parentReflection !== false) {
439
            $properties = array_merge(
440
                $properties,
441
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
442
            );
443
        }
444
445
        $this->properties[$reflectionClass->getName()] = $properties;
446
447
        return $properties;
448
    }
449
450
    /**
451
     * Parses analyzers list from document mapping.
452
     *
453
     * @param \ReflectionClass $reflectionClass
454
     * @return array
455
     */
456
    private function getAnalyzers(\ReflectionClass $reflectionClass)
457
    {
458
        $analyzers = [];
459
        $directory = $this->guessDirName($reflectionClass);
460
461
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
462
            $type = $this->getPropertyAnnotationData($property);
463
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
464
465
            if ($type instanceof Embedded) {
466
                $analyzers = array_merge(
467
                    $analyzers,
468
                    $this->getAnalyzers(new \ReflectionClass($this->finder->getNamespace($type->class, $directory)))
469
                );
470
            }
471
472
            if ($type instanceof Property) {
473
                if (isset($type->options['analyzer'])) {
474
                    $analyzers[] = $type->options['analyzer'];
475
                }
476
                if (isset($type->options['search_analyzer'])) {
477
                    $analyzers[] = $type->options['search_analyzer'];
478
                }
479
480
                if (isset($type->options['fields'])) {
481
                    foreach ($type->options['fields'] as $field) {
482
                        if (isset($field['analyzer'])) {
483
                            $analyzers[] = $field['analyzer'];
484
                        }
485
                        if (isset($field['search_analyzer'])) {
486
                            $analyzers[] = $field['search_analyzer'];
487
                        }
488
                    }
489
                }
490
            }
491
        }
492
        return array_unique($analyzers);
493
    }
494
495
    /**
496
     * Returns properties of reflection class.
497
     *
498
     * @param \ReflectionClass $reflectionClass Class to read properties from.
499
     * @param array            $properties      Properties to skip.
500
     * @param bool             $flag            If false exludes properties, true only includes properties.
501
     *
502
     * @return array
503
     */
504
    private function getProperties(\ReflectionClass $reflectionClass, $properties = [], $flag = false)
505
    {
506
        $mapping = [];
507
        $directory = $this->guessDirName($reflectionClass);
508
509
        /** @var \ReflectionProperty $property */
510
        foreach ($this->getDocumentPropertiesReflection($reflectionClass) as $name => $property) {
511
            $type = $this->getPropertyAnnotationData($property);
512
            $type = $type !== null ? $type : $this->getEmbeddedAnnotationData($property);
513
            $type = $type !== null ? $type : $this->getHashMapAnnotationData($property);
514
515
            if ((in_array($name, $properties) && !$flag)
516
                || (!in_array($name, $properties) && $flag)
517
                || empty($type)
518
            ) {
519
                continue;
520
            }
521
522
            $map = $type->dump();
523
524
            // Inner object
525
            if ($type instanceof Embedded) {
526
                $map = array_replace_recursive($map, $this->getObjectMapping($type->class, $directory));
527
            }
528
529
            // HashMap object
530
            if ($type instanceof HashMap) {
531
                $map = array_replace_recursive($map, [
532
                    'type' => Nested::NAME,
533
                    'dynamic' => true,
534
                ]);
535
            }
536
537
            // If there is set some Raw options, it will override current ones.
538
            if (isset($map['options'])) {
539
                $options = $map['options'];
540
                unset($map['options']);
541
                $map = array_merge($map, $options);
542
            }
543
544
            $mapping[$type->name] = $map;
545
        }
546
547
        return $mapping;
548
    }
549
550
    /**
551
     * Returns object mapping.
552
     *
553
     * Loads from cache if it's already loaded.
554
     *
555
     * @param string $className
556
     * @param string $directory Name of the directory where the Document is
557
     *
558
     * @return array
559
     */
560
    private function getObjectMapping($className, $directory)
561
    {
562
        $namespace = $this->finder->getNamespace($className, $directory);
563
564
        if (array_key_exists($namespace, $this->objects)) {
565
            return $this->objects[$namespace];
566
        }
567
568
        $reflectionClass = new \ReflectionClass($namespace);
569
570
        switch (true) {
571
            case $this->reader->getClassAnnotation($reflectionClass, self::OBJECT_ANNOTATION):
572
                $type = 'object';
573
                break;
574
            case $this->reader->getClassAnnotation($reflectionClass, self::NESTED_ANNOTATION):
575
                $type = 'nested';
576
                break;
577
            default:
578
                throw new \LogicException(
579
                    sprintf(
580
                        '%s should have @Object or @Nested annotation to be used as embeddable object.',
581
                        $className
582
                    )
583
                );
584
        }
585
586
        $this->objects[$namespace] = [
587
            'type' => $type,
588
            'properties' => $this->getProperties($reflectionClass),
589
        ];
590
591
        return $this->objects[$namespace];
592
    }
593
594
    /**
595
     * @param \ReflectionClass $reflection
596
     *
597
     * @return string
598
     */
599
    private function guessDirName(\ReflectionClass $reflection)
600
    {
601
        return substr(
602
            $directory = $reflection->getName(),
603
            $start = strpos($directory, '\\') + 1,
604
            strrpos($directory, '\\') - $start
605
        );
606
    }
607
}
608