Completed
Pull Request — master (#941)
by
unknown
02:20
created

DocumentParser::getPropertyMetadata()   F

Complexity

Conditions 18
Paths 298

Size

Total Lines 109

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 109
rs 2.2466
c 0
b 0
f 0
cc 18
nc 298
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\Common\Cache\Cache;
17
use ONGR\ElasticsearchBundle\Annotation\AbstractAnnotation;
18
use ONGR\ElasticsearchBundle\Annotation\Embedded;
19
use ONGR\ElasticsearchBundle\Annotation\Id;
20
use ONGR\ElasticsearchBundle\Annotation\Index;
21
use ONGR\ElasticsearchBundle\Annotation\NestedType;
22
use ONGR\ElasticsearchBundle\Annotation\ObjectType;
23
use ONGR\ElasticsearchBundle\Annotation\PropertiesAwareInterface;
24
use ONGR\ElasticsearchBundle\Annotation\Property;
25
26
/**
27
 * Document parser used for reading document annotations.
28
 */
29
class DocumentParser
30
{
31
    const OBJ_CACHED_FIELDS = 'ongr.obj_fields';
32
    const EMBEDDED_CACHED_FIELDS = 'ongr.embedded_fields';
33
    const ARRAY_CACHED_FIELDS = 'ongr.array_fields';
34
35
    private $reader;
36
    private $properties = [];
37
    private $methods = [];
38
    private $analysisConfig = [];
39
    private $cache;
40
41
    public function __construct(Reader $reader, Cache $cache, array $analysisConfig = [])
42
    {
43
        $this->reader = $reader;
44
        $this->cache = $cache;
45
        $this->analysisConfig = $analysisConfig;
46
47
        #Fix for annotations loader until doctrine/annotations 2.0 will be released with the full autoload support.
48
        AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...istry::registerLoader() 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...
49
    }
50
51
    public function getIndexAliasName(\ReflectionClass $class): string
52
    {
53
        /** @var Index $document */
54
        $document = $this->reader->getClassAnnotation($class, Index::class);
55
56
        return $document->alias ?? Caser::snake($class->getShortName());
57
    }
58
59
    public function isDefaultIndex(\ReflectionClass $class): bool
60
    {
61
        /** @var Index $document */
62
        $document = $this->reader->getClassAnnotation($class, Index::class);
63
64
        return $document->default;
65
    }
66
67
    public function getIndexAnnotation(\ReflectionClass $class)
68
    {
69
        /** @var Index $document */
70
        $document = $this->reader->getClassAnnotation($class, Index::class);
71
72
        return $document;
73
    }
74
75
    public function getIndexMetadata(\ReflectionClass $class): array
76
    {
77
        if ($class->isTrait()) {
78
            return [];
79
        }
80
81
        /** @var Index $document */
82
        $document = $this->reader->getClassAnnotation($class, Index::class);
83
84
        if ($document === null) {
85
            return [];
86
        }
87
88
        $settings = $document->getSettings();
89
        $settings['analysis'] = $this->getAnalysisConfig($class);
90
91
        return array_filter(array_map('array_filter', [
92
            'settings' => $settings,
93
            'mappings' => [
94
                'properties' => array_filter($this->getClassMetadata($class))
95
            ]
96
        ]));
97
    }
98
99
    private function getClassMetadata(\ReflectionClass $class): array
100
    {
101
        $mapping = [];
102
        $objFields = null;
103
        $arrayFields = null;
104
        $embeddedFields = null;
105
106
        /** @var \ReflectionProperty $property */
107
        foreach ($this->getDocumentPropertiesReflection($class) as $name => $property) {
108
            $annotations = $this->reader->getPropertyAnnotations($property);
109
            $this->getPropertyMapping($annotations, $embeddedFields, $name, $mapping, $objFields, $arrayFields);
0 ignored issues
show
Bug introduced by
It seems like $objFields defined by null on line 102 can also be of type null; however, ONGR\ElasticsearchBundle...r::getPropertyMapping() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $arrayFields defined by null on line 103 can also be of type null; however, ONGR\ElasticsearchBundle...r::getPropertyMapping() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
110
        }
111
112
        /** @var \ReflectionMethod $method */
113
        foreach ($this->getDocumentMethodsReflection($class) as $name => $method) {
114
            $name = $this->guessPropertyNameFromGetter($name);
115
            $annotations = $this->reader->getMethodAnnotations($method);
116
            $this->getPropertyMapping($annotations, $embeddedFields, $name, $mapping, $objFields, $arrayFields);
0 ignored issues
show
Bug introduced by
It seems like $objFields defined by null on line 102 can also be of type null; however, ONGR\ElasticsearchBundle...r::getPropertyMapping() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $arrayFields defined by null on line 103 can also be of type null; however, ONGR\ElasticsearchBundle...r::getPropertyMapping() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
117
        }
118
119
        //Embeded fields are option compared to the array or object mapping.
120
        if ($embeddedFields) {
121
            $cacheItem = $this->cache->fetch(self::EMBEDDED_CACHED_FIELDS) ?? [];
122
            $cacheItem[$class->getName()] = $embeddedFields;
123
            $t = $this->cache->save(self::EMBEDDED_CACHED_FIELDS, $cacheItem);
0 ignored issues
show
Unused Code introduced by
$t is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
124
        }
125
126
        $cacheItem = $this->cache->fetch(self::ARRAY_CACHED_FIELDS) ?? [];
127
        $cacheItem[$class->getName()] = $arrayFields;
128
        $this->cache->save(self::ARRAY_CACHED_FIELDS, $cacheItem);
129
130
        $cacheItem = $this->cache->fetch(self::OBJ_CACHED_FIELDS) ?? [];
131
        $cacheItem[$class->getName()] = $objFields;
132
        $this->cache->save(self::OBJ_CACHED_FIELDS, $cacheItem);
133
134
        return $mapping;
135
    }
136
137
    public function getPropertyMetadata(\ReflectionClass $class, bool $subClass = false): array
138
    {
139
        if ($class->isTrait() || (!$this->reader->getClassAnnotation($class, Index::class) && !$subClass)) {
140
            return [];
141
        }
142
143
        $metadata = [];
144
145
        /** @var \ReflectionProperty $method */
146
        foreach ($this->getDocumentPropertiesReflection($class) as $name => $method) {
147
            /** @var AbstractAnnotation $annotation */
148
            foreach ($this->reader->getPropertyAnnotations($method) as $annotation) {
149
                if (!$annotation instanceof PropertiesAwareInterface) {
150
                    continue;
151
                }
152
153
                $propertyMetadata = [
154
                    'identifier' => false,
155
                    'class' => null,
156
                    'embeded' => false,
157
                    'type' => null,
158
                    'public' => $method->isPublic(),
159
                    'getter' => null,
160
                    'setter' => null,
161
                    'sub_properties' => []
162
                ];
163
164
                $name = $method->getName();
165
                $propertyMetadata['name'] = $name;
166
167
                if (!$propertyMetadata['public']) {
168
                    $propertyMetadata['getter'] = $this->guessGetter($class, $name);
169
                }
170
171
                if ($annotation instanceof Id) {
172
                    $propertyMetadata['identifier'] = true;
173
                } else {
174
                    if (!$propertyMetadata['public']) {
175
                        $propertyMetadata['setter'] = $this->guessSetter($class, $name);
176
                    }
177
                }
178
179
                if ($annotation instanceof Property) {
180
                    // we need the type (and possibly settings?) in Converter::denormalize()
181
                    $propertyMetadata['type'] = $annotation->type;
182
                    $propertyMetadata['settings'] = $annotation->settings;
183
                }
184
185
                if ($annotation instanceof Embedded) {
186
                    $propertyMetadata['embeded'] = true;
187
                    $propertyMetadata['class'] = $annotation->class;
188
                    $propertyMetadata['sub_properties'] = $this->getPropertyMetadata(
189
                        new \ReflectionClass($annotation->class),
190
                        true
191
                    );
192
                }
193
194
                $metadata[$annotation->getName() ?? Caser::snake($name)] = $propertyMetadata;
195
            }
196
        }
197
198
        /** @var \ReflectionProperty $property */
199
        foreach ($this->getDocumentMethodsReflection($class) as $name => $method) {
200
            /** @var AbstractAnnotation $annotation */
201
            foreach ($this->reader->getMethodAnnotations($method) as $annotation) {
202
                if (!$annotation instanceof PropertiesAwareInterface) {
203
                    continue;
204
                }
205
206
                $guessedName = $this->guessPropertyNameFromGetter($name);
207
                $fieldName = $annotation->getName() ?? Caser::snake($guessedName);
208
209
                $propertyMetadata = [
210
                    'identifier' => false,
211
                    'class' => null,
212
                    'embeded' => false,
213
                    'type' => null,
214
                    'public' => false,
215
                    'getter' => $name,
216
                    'setter' => null,
217
                    'sub_properties' => [],
218
                    'name' => $annotation->getName() ?? $guessedName
219
                ];
220
221
                if ($annotation instanceof Id) {
222
                    $propertyMetadata['identifier'] = true;
223
                }
224
225
                if ($annotation instanceof Property) {
226
                    // we need the type (and possibly settings?) in Converter::denormalize()
227
                    $propertyMetadata['type'] = $annotation->type;
228
                    $propertyMetadata['settings'] = $annotation->settings;
229
                }
230
231
                if ($annotation instanceof Embedded) {
232
                    $propertyMetadata['embeded'] = true;
233
                    $propertyMetadata['class'] = $annotation->class;
234
                    $propertyMetadata['sub_properties'] = $this->getPropertyMetadata(
235
                        new \ReflectionClass($annotation->class),
236
                        true
237
                    );
238
                }
239
240
                $metadata[$fieldName] = $propertyMetadata;
241
            }
242
        }
243
244
        return $metadata;
245
    }
246
247
    public function getAnalysisConfig(\ReflectionClass $class): array
248
    {
249
        $config = [];
250
        $mapping = $this->getClassMetadata($class);
251
252
        //Think how to remove these array merge
253
        $analyzers = $this->getListFromArrayByKey('analyzer', $mapping);
254
        $analyzers = array_merge($analyzers, $this->getListFromArrayByKey('search_analyzer', $mapping));
255
        $analyzers = array_merge($analyzers, $this->getListFromArrayByKey('search_quote_analyzer', $mapping));
256
257
        foreach ($analyzers as $analyzer) {
258
            if (isset($this->analysisConfig['analyzer'][$analyzer])) {
259
                $config['analyzer'][$analyzer] = $this->analysisConfig['analyzer'][$analyzer];
260
            }
261
        }
262
263
        $normalizers = $this->getListFromArrayByKey('normalizer', $mapping);
264
        foreach ($normalizers as $normalizer) {
265
            if (isset($this->analysisConfig['normalizer'][$normalizer])) {
266
                $config['normalizer'][$normalizer] = $this->analysisConfig['normalizer'][$normalizer];
267
            }
268
        }
269
270
        foreach (['tokenizer', 'filter', 'char_filter'] as $type) {
271
            $list = $this->getListFromArrayByKey($type, $config);
272
273
            foreach ($list as $listItem) {
274
                if (isset($this->analysisConfig[$type][$listItem])) {
275
                    $config[$type][$listItem] = $this->analysisConfig[$type][$listItem];
276
                }
277
            }
278
        }
279
280
        return $config;
281
    }
282
283
    protected function guessGetter(\ReflectionClass $class, $name): string
284
    {
285
        if ($class->hasMethod($name)) {
286
            return $name;
287
        }
288
289
        if ($class->hasMethod('get' . ucfirst($name))) {
290
            return 'get' . ucfirst($name);
291
        }
292
293
        if ($class->hasMethod('is' . ucfirst($name))) {
294
            return 'is' . ucfirst($name);
295
        }
296
297
        // if there are underscores in the name convert them to CamelCase
298
        if (strpos($name, '_')) {
299
            $name = Caser::camel($name);
300
            if ($class->hasMethod('get' . ucfirst($name))) {
301
                return 'get' . $name;
302
            }
303
            if ($class->hasMethod('is' . ucfirst($name))) {
304
                return 'is' . $name;
305
            }
306
        }
307
308
        throw new \Exception("Could not determine a getter for `$name` of class `{$class->getNamespaceName()}`");
309
    }
310
311
    protected function guessPropertyNameFromGetter($name): string
312
    {
313
        if (preg_match('/^get([A-Z_0-9].*?)$/', $name, $matches)) {
314
            return lcfirst($matches[1]);
315
        }
316
317
        return $name;
318
    }
319
320
    protected function guessSetter(\ReflectionClass $class, $name): string
321
    {
322
        if ($class->hasMethod('set' . ucfirst($name))) {
323
            return 'set' . ucfirst($name);
324
        }
325
326
        // if there are underscores in the name convert them to CamelCase
327
        if (strpos($name, '_')) {
328
            $name = Caser::camel($name);
329
            if ($class->hasMethod('set' . ucfirst($name))) {
330
                return 'set' . $name;
331
            }
332
        }
333
334
        throw new \Exception("Could not determine a setter for `$name` of class `{$class->getNamespaceName()}`");
335
    }
336
337
    private function getListFromArrayByKey(string $searchKey, array $array): array
338
    {
339
        $list = [];
340
341
        foreach (new \RecursiveIteratorIterator(
342
            new \RecursiveArrayIterator($array),
343
            \RecursiveIteratorIterator::SELF_FIRST
344
        ) as $key => $value) {
345
            if ($key === $searchKey) {
346
                if (is_array($value)) {
347
                    $list = array_merge($list, $value);
348
                } else {
349
                    $list[] = $value;
350
                }
351
            }
352
        }
353
354
        return array_unique($list);
355
    }
356
357
    private function getObjectMappingType(\ReflectionClass $class): string
358
    {
359
        switch (true) {
360
            case $this->reader->getClassAnnotation($class, ObjectType::class):
361
                $type = ObjectType::TYPE;
362
                break;
363
            case $this->reader->getClassAnnotation($class, NestedType::class):
364
                $type = NestedType::TYPE;
365
                break;
366
            default:
367
                throw new \LogicException(
368
                    sprintf(
369
                        '%s must be used @ObjectType or @NestedType as embeddable object.',
370
                        $class->getName()
371
                    )
372
                );
373
        }
374
375
        return $type;
376
    }
377
378
    private function getDocumentPropertiesReflection(\ReflectionClass $class): array
379
    {
380
        if (in_array($class->getName(), $this->properties)) {
381
            return $this->properties[$class->getName()];
382
        }
383
384
        $properties = [];
385
386
        foreach ($class->getProperties() as $property) {
387
            if (!in_array($property->getName(), $properties)) {
388
                $properties[$property->getName()] = $property;
389
            }
390
        }
391
392
        foreach ($class->getMethods() as $method) {
393
394
            if (!in_array($property->getName(), $properties)) {
395
                $properties[$property->getName()] = $property;
0 ignored issues
show
Bug introduced by
The variable $property seems to be defined by a foreach iteration on line 386. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
396
            }
397
        }
398
399
        $parentReflection = $class->getParentClass();
400
        if ($parentReflection !== false) {
401
            $properties = array_merge(
402
                $properties,
403
                array_diff_key($this->getDocumentPropertiesReflection($parentReflection), $properties)
404
            );
405
        }
406
407
        $this->properties[$class->getName()] = $properties;
408
409
        return $properties;
410
    }
411
412
    private function getDocumentMethodsReflection(\ReflectionClass $class): array
413
    {
414
        if (in_array($class->getName(), $this->methods)) {
415
            return $this->methods[$class->getName()];
416
        }
417
418
        $methods = [];
419
420
        foreach ($class->getMethods() as $method) {
421
            if (!in_array($method->getName(), $methods)) {
422
                $methods[$method->getName()] = $method;
423
            }
424
        }
425
426
        $parentReflection = $class->getParentClass();
427
        if ($parentReflection !== false) {
428
            $methods = array_merge(
429
                $methods,
430
                array_diff_key($this->getDocumentMethodsReflection($parentReflection), $methods)
431
            );
432
        }
433
434
        $this->methods[$class->getName()] = $methods;
435
436
        return $methods;
437
    }
438
439
    /**
440
     * @param array $annotations
441
     * @param $embeddedFields
442
     * @param string $name
443
     * @param array $mapping
444
     * @param array $objFields
445
     * @param array $arrayFields
446
     * @throws \ReflectionException
447
     */
448
    private function getPropertyMapping(array $annotations, &$embeddedFields, string $name, array &$mapping, ?array &$objFields, ?array &$arrayFields): void
449
    {
450
        /** @var AbstractAnnotation $annotation */
451
        foreach ($annotations as $annotation) {
452
            if (!$annotation instanceof PropertiesAwareInterface) {
453
                continue;
454
            }
455
456
            $fieldMapping = $annotation->getSettings();
457
458
            if ($annotation instanceof Property) {
459
                $fieldMapping['type'] = $annotation->type;
460
                if ($annotation->fields) {
461
                    $fieldMapping['fields'] = $annotation->fields;
462
                }
463
                $fieldMapping['analyzer']              = $annotation->analyzer;
464
                $fieldMapping['search_analyzer']       = $annotation->searchAnalyzer;
465
                $fieldMapping['search_quote_analyzer'] = $annotation->searchQuoteAnalyzer;
466
            }
467
468
            if ($annotation instanceof Embedded) {
469
                $embeddedClass              = new \ReflectionClass($annotation->class);
470
                $fieldMapping['type']       = $this->getObjectMappingType($embeddedClass);
471
                $fieldMapping['properties'] = $this->getClassMetadata($embeddedClass);
472
                $embeddedFields[$name]      = $annotation->class;
473
            }
474
475
            $fieldName               = $annotation->getName() ?? Caser::snake($name);
476
            $mapping[$fieldName]     = array_filter($fieldMapping);
477
            $objFields[$name]        = $fieldName;
478
            $arrayFields[$fieldName] = $name;
479
        }
480
    }
481
}
482