Completed
Push — feature/no-index-regeneration ( d3d884 )
by Narcotic
62:08
created

DocumentMap   D

Complexity

Total Complexity 89

Size/Duplication

Total Lines 553
Duplicated Lines 11.93 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 89
lcom 1
cbo 7
dl 66
loc 553
rs 4.8717
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 20 5
A getDocument() 0 17 3
A getDocuments() 0 4 1
F processDocument() 0 117 37
A loadDoctrineClassMap() 0 20 3
A loadSerializerClassMap() 21 21 2
D loadSchemaClassMap() 6 32 10
A loadValidationClassMap() 22 22 2
A getSerializerFields() 0 17 2
A getSerializerFieldType() 0 11 3
A getValidationFields() 0 16 1
A getDoctrineFields() 0 21 3
A getDoctrineEmbedOneFields() 0 4 1
A getDoctrineEmbedManyFields() 0 4 1
B getRelationList() 6 25 5
C getFieldNamesFlat() 11 46 8
A getFlatFieldCheckCallback() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocumentMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentMap, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * DocumentMap class file
4
 */
5
6
namespace Graviton\DocumentBundle\DependencyInjection\Compiler\Utils;
7
8
use Symfony\Component\Finder\Finder;
9
use Symfony\Component\Yaml\Yaml;
10
11
/**
12
 * Document map
13
 *
14
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
15
 * @license  https://opensource.org/licenses/MIT MIT License
16
 * @link     http://swisscom.ch
17
 */
18
class DocumentMap
19
{
20
    /**
21
     * @var array
22
     */
23
    private $mappings = [];
24
    /**
25
     * @var Document[]
26
     */
27
    private $documents = [];
28
29
    /**
30
     * Constructor
31
     *
32
     * @param Finder $doctrineFinder   Doctrine mapping finder
33
     * @param Finder $serializerFinder Serializer mapping finder
34
     * @param Finder $validationFinder Validation mapping finder
35
     * @param Finder $schemaFinder     Schema finder
36
     */
37
    public function __construct(
38
        Finder $doctrineFinder,
39
        Finder $serializerFinder,
40
        Finder $validationFinder,
41
        Finder $schemaFinder
42
    ) {
43
        $doctrineMap = $this->loadDoctrineClassMap($doctrineFinder);
44
        $serializerMap = $this->loadSerializerClassMap($serializerFinder);
45
        $validationMap = $this->loadValidationClassMap($validationFinder);
46
        $schemaMap = $this->loadSchemaClassMap($schemaFinder);
47
48
        foreach ($doctrineMap as $className => $doctrineMapping) {
49
            $this->mappings[$className] = [
50
                'doctrine'   => $doctrineMap[$className],
51
                'serializer' => isset($serializerMap[$className]) ? $serializerMap[$className] : null,
52
                'validation' => isset($validationMap[$className]) ? $validationMap[$className] : null,
53
                'schema' => isset($schemaMap[$className]) ? $schemaMap[$className] : null,
54
            ];
55
        }
56
    }
57
58
    /**
59
     * Get document
60
     *
61
     * @param string $className Document class
62
     * @return Document
63
     */
64
    public function getDocument($className)
65
    {
66
        if (isset($this->documents[$className])) {
67
            return $this->documents[$className];
68
        }
69
        if (!isset($this->mappings[$className])) {
70
            throw new \InvalidArgumentException(sprintf('No XML mapping found for document "%s"', $className));
71
        }
72
73
        return $this->documents[$className] = $this->processDocument(
74
            $className,
75
            $this->mappings[$className]['doctrine'],
76
            $this->mappings[$className]['serializer'],
77
            $this->mappings[$className]['validation'],
78
            $this->mappings[$className]['schema']
79
        );
80
    }
81
82
    /**
83
     * Get all documents
84
     *
85
     * @return Document[]
86
     */
87
    public function getDocuments()
88
    {
89
        return array_map([$this, 'getDocument'], array_keys($this->mappings));
90
    }
91
92
    /**
93
     * Process document
94
     *
95
     * @param string      $className         Class name
96
     * @param array       $doctrineMapping   Doctrine mapping
97
     * @param \DOMElement $serializerMapping Serializer XML mapping
0 ignored issues
show
Documentation introduced by
Should the type for parameter $serializerMapping not be null|\DOMElement?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
98
     * @param \DOMElement $validationMapping Validation XML mapping
0 ignored issues
show
Documentation introduced by
Should the type for parameter $validationMapping not be null|\DOMElement?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
99
     * @param array       $schemaMapping     Schema mapping
0 ignored issues
show
Documentation introduced by
Should the type for parameter $schemaMapping not be null|array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
100
     *
101
     * @return Document
102
     */
103
    private function processDocument(
104
        $className,
105
        array $doctrineMapping,
106
        \DOMElement $serializerMapping = null,
107
        \DOMElement $validationMapping = null,
108
        array $schemaMapping = null
109
    ) {
110
        if ($serializerMapping === null) {
111
            $serializerFields = [];
112
        } else {
113
            $serializerFields = array_reduce(
114
                $this->getSerializerFields($serializerMapping),
115
                function (array $fields, array $field) {
116
                    $fields[$field['fieldName']] = $field;
117
                    return $fields;
118
                },
119
                []
120
            );
121
        }
122
123
        if ($validationMapping === null) {
124
            $validationFields = [];
125
        } else {
126
            $validationFields = array_reduce(
127
                $this->getValidationFields($validationMapping),
128
                function (array $fields, array $field) {
129
                    $fields[$field['fieldName']] = $field;
130
                    return $fields;
131
                },
132
                []
133
            );
134
        }
135
136
        if ($schemaMapping === null) {
137
            $schemaFields = [];
138
        } else {
139
            $schemaFields = $schemaMapping;
140
        }
141
142
        $fields = [];
143
        foreach ($this->getDoctrineFields($doctrineMapping) as $doctrineField) {
144
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
145
                $serializerFields[$doctrineField['name']] :
146
                null;
147
            $validationField = isset($validationFields[$doctrineField['name']]) ?
148
                $validationFields[$doctrineField['name']] :
149
                null;
150
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
151
                $schemaFields[$doctrineField['name']] :
152
                null;
153
154
            if ($doctrineField['type'] === 'collection') {
155
                $fields[] = new ArrayField(
156
                    $serializerField === null ? 'array<string>' : $serializerField['fieldType'],
157
                    $doctrineField['name'],
158
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
159
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
160
                    $validationField === null ? false : $validationField['required'],
161
                    $serializerField === null ? false : $serializerField['searchable'],
162
                    !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
0 ignored issues
show
Unused Code introduced by
The call to ArrayField::__construct() has too many arguments starting with !isset($schemaField['rec...recordOriginException'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
163
                );
164
            } else {
165
                $fields[] = new Field(
166
                    $doctrineField['type'],
167
                    $doctrineField['name'],
168
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
169
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
170
                    $validationField === null ? false : $validationField['required'],
171
                    $serializerField === null ? false : $serializerField['searchable'],
172
                    !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
173
                );
174
            }
175
        }
176
        foreach ($this->getDoctrineEmbedOneFields($doctrineMapping) as $doctrineField) {
177
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
178
                $serializerFields[$doctrineField['name']] :
179
                null;
180
            $validationField = isset($validationFields[$doctrineField['name']]) ?
181
                $validationFields[$doctrineField['name']] :
182
                null;
183
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
184
                $schemaFields[$doctrineField['name']] :
185
                null;
186
187
            $fields[] = new EmbedOne(
188
                $this->getDocument($doctrineField['type']),
189
                $doctrineField['name'],
190
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
191
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
192
                $validationField === null ? false : $validationField['required'],
193
                $serializerField === null ? false : $serializerField['searchable'],
194
                !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
0 ignored issues
show
Unused Code introduced by
The call to EmbedOne::__construct() has too many arguments starting with !isset($schemaField['rec...recordOriginException'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
195
            );
196
        }
197
        foreach ($this->getDoctrineEmbedManyFields($doctrineMapping) as $doctrineField) {
198
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
199
                $serializerFields[$doctrineField['name']] :
200
                null;
201
            $validationField = isset($validationFields[$doctrineField['name']]) ?
202
                $validationFields[$doctrineField['name']] :
203
                null;
204
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
205
                $schemaFields[$doctrineField['name']] :
206
                null;
207
208
            $fields[] = new EmbedMany(
209
                $this->getDocument($doctrineField['type']),
210
                $doctrineField['name'],
211
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
212
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
213
                $validationField === null ? false : $validationField['required'],
214
                !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
215
            );
216
        }
217
218
        return new Document($className, $fields);
219
    }
220
221
    /**
222
     * Load doctrine class map
223
     *
224
     * @param Finder $finder Mapping finder
225
     * @return array
226
     */
227
    private function loadDoctrineClassMap(Finder $finder)
228
    {
229
        $classMap = [];
230
        foreach ($finder as $file) {
231
            $classMap = array_merge(
232
                $classMap,
233
                Yaml::parseFile($file)
234
            );
235
        }
236
237
        // filter out superclasses
238
        $classMap = array_filter(
239
            $classMap,
240
            function ($classEntry) {
241
                return (!isset($classEntry['type']) || $classEntry['type'] != 'mappedSuperclass');
242
            }
243
        );
244
245
        return $classMap;
246
    }
247
248
    /**
249
     * Load serializer class map
250
     *
251
     * @param Finder $finder Mapping finder
252
     * @return array
253
     */
254 View Code Duplication
    private function loadSerializerClassMap(Finder $finder)
255
    {
256
        $classMap = [];
257
        foreach ($finder as $file) {
258
            $document = new \DOMDocument();
259
            $document->load($file);
260
261
            $xpath = new \DOMXPath($document);
262
263
            $classMap = array_reduce(
264
                iterator_to_array($xpath->query('//class')),
265
                function (array $classMap, \DOMElement $element) {
266
                    $classMap[$element->getAttribute('name')] = $element;
267
                    return $classMap;
268
                },
269
                $classMap
270
            );
271
        }
272
273
        return $classMap;
274
    }
275
276
    /**
277
     * Load schema class map
278
     *
279
     * @param Finder $finder Mapping finder
280
     * @return array
281
     */
282
    private function loadSchemaClassMap(Finder $finder)
283
    {
284
        $classMap = [];
285
        foreach ($finder as $file) {
286
            $schema = json_decode(file_get_contents($file), true);
287
288
            if (!isset($schema['x-documentClass'])) {
289
                continue;
290
            }
291
292 View Code Duplication
            foreach ($schema['required'] as $field) {
293
                $classMap[$schema['x-documentClass']][$field]['required'] = true;
294
            }
295
            foreach ($schema['searchable'] as $field) {
296
                $classMap[$schema['x-documentClass']][$field]['searchable'] = 1;
297
            }
298 View Code Duplication
            foreach ($schema['readOnlyFields'] as $field) {
299
                $classMap[$schema['x-documentClass']][$field]['readOnly'] = true;
300
            }
301
302
            // flags from fields
303
            if (is_array($schema['properties'])) {
304
                foreach ($schema['properties'] as $fieldName => $field) {
305
                    if (isset($field['recordOriginException']) && $field['recordOriginException'] == true) {
306
                        $classMap[$schema['x-documentClass']][$fieldName]['recordOriginException'] = true;
307
                    }
308
                }
309
            }
310
        }
311
312
        return $classMap;
313
    }
314
315
    /**
316
     * Load validation class map
317
     *
318
     * @param Finder $finder Mapping finder
319
     * @return array
320
     */
321 View Code Duplication
    private function loadValidationClassMap(Finder $finder)
322
    {
323
        $classMap = [];
324
        foreach ($finder as $file) {
325
            $document = new \DOMDocument();
326
            $document->load($file);
327
328
            $xpath = new \DOMXPath($document);
329
            $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
330
331
            $classMap = array_reduce(
332
                iterator_to_array($xpath->query('//constraint:class')),
333
                function (array $classMap, \DOMElement $element) {
334
                    $classMap[$element->getAttribute('name')] = $element;
335
                    return $classMap;
336
                },
337
                $classMap
338
            );
339
        }
340
341
        return $classMap;
342
    }
343
344
    /**
345
     * Get serializer fields
346
     *
347
     * @param \DOMElement $mapping Serializer XML mapping
348
     * @return array
349
     */
350
    private function getSerializerFields(\DOMElement $mapping)
351
    {
352
        $xpath = new \DOMXPath($mapping->ownerDocument);
353
354
        return array_map(
355
            function (\DOMElement $element) {
356
                return [
357
                    'fieldName'   => $element->getAttribute('name'),
358
                    'fieldType'   => $this->getSerializerFieldType($element),
359
                    'exposedName' => $element->getAttribute('serialized-name') ?: $element->getAttribute('name'),
360
                    'readOnly'    => $element->getAttribute('read-only') === 'true',
361
                    'searchable'  => (int) $element->getAttribute('searchable')
362
                ];
363
            },
364
            iterator_to_array($xpath->query('property', $mapping))
365
        );
366
    }
367
368
    /**
369
     * Get serializer field type
370
     *
371
     * @param \DOMElement $field Field node
372
     * @return string|null
373
     */
374
    private function getSerializerFieldType(\DOMElement $field)
375
    {
376
        if ($field->getAttribute('type')) {
377
            return $field->getAttribute('type');
378
        }
379
380
        $xpath = new \DOMXPath($field->ownerDocument);
381
382
        $type = $xpath->query('type', $field)->item(0);
383
        return $type === null ? null : $type->nodeValue;
384
    }
385
386
    /**
387
     * Get validation fields
388
     *
389
     * @param \DOMElement $mapping Validation XML mapping
390
     * @return array
391
     */
392
    private function getValidationFields(\DOMElement $mapping)
393
    {
394
        $xpath = new \DOMXPath($mapping->ownerDocument);
395
        $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
396
397
        return array_map(
398
            function (\DOMElement $element) use ($xpath) {
399
                $constraints = $xpath->query('constraint:constraint[@name="NotBlank" or @name="NotNull"]', $element);
400
                return [
401
                    'fieldName' => $element->getAttribute('name'),
402
                    'required'  => $constraints->length > 0,
403
                ];
404
            },
405
            iterator_to_array($xpath->query('constraint:property', $mapping))
406
        );
407
    }
408
409
    /**
410
     * Get doctrine document fields
411
     *
412
     * @param array $mapping Doctrine mapping
413
     * @return array
414
     */
415
    private function getDoctrineFields(array $mapping)
416
    {
417
        if (!isset($mapping['fields'])) {
418
            return [];
419
        }
420
421
        return array_map(
422
            function ($key, $value) {
423
                if (!isset($value['type'])) {
424
                    $value['type'] = '';
425
                }
426
427
                return [
428
                    'name' => $key,
429
                    'type' => $value['type']
430
                ];
431
            },
432
            array_keys($mapping['fields']),
433
            $mapping['fields']
434
        );
435
    }
436
437
    /**
438
     * Get doctrine document embed-one fields
439
     *
440
     * @param array $mapping Doctrine mapping
441
     * @return array
442
     */
443
    private function getDoctrineEmbedOneFields(array $mapping)
444
    {
445
        return $this->getRelationList($mapping, 'One');
446
    }
447
448
    /**
449
     * Get doctrine document embed-many fields
450
     *
451
     * @param array $mapping Doctrine mapping
452
     * @return array
453
     */
454
    private function getDoctrineEmbedManyFields(array $mapping)
455
    {
456
        return $this->getRelationList($mapping, 'Many');
457
    }
458
459
    /**
460
     * gets list of relations
461
     *
462
     * @param array  $mapping mapping
463
     * @param string $suffix  suffix
464
     *
465
     * @return array relations
466
     */
467
    private function getRelationList($mapping, $suffix)
468
    {
469
        if (!isset($mapping['embed'.$suffix]) && !isset($mapping['reference'.$suffix])) {
470
            return [];
471
        }
472
473
        $relations = [];
474 View Code Duplication
        if (isset($mapping['embed'.$suffix])) {
475
            $relations = array_merge($relations, $mapping['embed'.$suffix]);
476
        }
477 View Code Duplication
        if (isset($mapping['reference'.$suffix])) {
478
            $relations = array_merge($relations, $mapping['reference'.$suffix]);
479
        }
480
481
        return array_map(
482
            function ($key, $value) {
483
                return [
484
                    'name' => $key,
485
                    'type' => $value['targetDocument']
486
                ];
487
            },
488
            array_keys($relations),
489
            $relations
490
        );
491
    }
492
493
    /**
494
     * Gets an array of all fields, flat with full internal name in dot notation as key and
495
     * the exposed field name as value. You can pass a callable to limit the fields return a subset of fields.
496
     * If the callback returns true, the field will be included in the output. You will get the field definition
497
     * passed to your callback.
498
     *
499
     * @param Document $document       The document
500
     * @param string   $documentPrefix Document field prefix
501
     * @param string   $exposedPrefix  Exposed field prefix
502
     * @param callable $callback       An optional callback where you can influence the number of fields returned
0 ignored issues
show
Documentation introduced by
Should the type for parameter $callback not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
503
     *
504
     * @return array
505
     */
506
    public function getFieldNamesFlat(
507
        Document $document,
508
        $documentPrefix = '',
509
        $exposedPrefix = '',
510
        callable $callback = null
511
    ) {
512
        $result = [];
513
        foreach ($document->getFields() as $field) {
514 View Code Duplication
            if ($this->getFlatFieldCheckCallback($field, $callback)) {
515
                $result[$documentPrefix . $field->getFieldName()] = $exposedPrefix . $field->getExposedName();
516
            }
517
518
            if ($field instanceof ArrayField) {
519 View Code Duplication
                if ($this->getFlatFieldCheckCallback($field, $callback)) {
520
                    $result[$documentPrefix . $field->getFieldName() . '.0'] =
521
                        $exposedPrefix . $field->getExposedName() . '.0';
522
                }
523
            } elseif ($field instanceof EmbedOne) {
524
                $result = array_merge(
525
                    $result,
526
                    $this->getFieldNamesFlat(
527
                        $field->getDocument(),
528
                        $documentPrefix.$field->getFieldName().'.',
529
                        $exposedPrefix.$field->getExposedName().'.',
530
                        $callback
531
                    )
532
                );
533
            } elseif ($field instanceof EmbedMany) {
534 View Code Duplication
                if ($this->getFlatFieldCheckCallback($field, $callback)) {
535
                    $result[$documentPrefix . $field->getFieldName() . '.0'] =
536
                        $exposedPrefix . $field->getExposedName() . '.0';
537
                }
538
                $result = array_merge(
539
                    $result,
540
                    $this->getFieldNamesFlat(
541
                        $field->getDocument(),
542
                        $documentPrefix.$field->getFieldName().'.0.',
543
                        $exposedPrefix.$field->getExposedName().'.0.',
544
                        $callback
545
                    )
546
                );
547
            }
548
        }
549
550
        return $result;
551
    }
552
553
    /**
554
     * Simple function to check whether a given shall be returned in the output of getFieldNamesFlat
555
     * and the optional given callback there.
556
     *
557
     * @param AbstractField $field    field
558
     * @param callable|null $callback optional callback
559
     *
560
     * @return bool|mixed true if field should be returned, false otherwise
561
     */
562
    private function getFlatFieldCheckCallback($field, callable $callback = null)
563
    {
564
        if (!is_callable($callback)) {
565
            return true;
566
        }
567
568
        return call_user_func($callback, $field);
569
    }
570
}
571