Completed
Pull Request — develop (#645)
by Narcotic
126:37 queued 61:38
created

DocumentMap::getRelationList()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 6
Ratio 24 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 0
Metric Value
dl 6
loc 25
ccs 7
cts 7
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 15
nc 5
nop 2
crap 5
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 10
     */
37
    public function __construct(
38
        Finder $doctrineFinder,
39
        Finder $serializerFinder,
40
        Finder $validationFinder,
41
        Finder $schemaFinder
42 10
    ) {
43 10
        $doctrineMap = $this->loadDoctrineClassMap($doctrineFinder);
44 10
        $serializerMap = $this->loadSerializerClassMap($serializerFinder);
45 10
        $validationMap = $this->loadValidationClassMap($validationFinder);
46
        $schemaMap = $this->loadSchemaClassMap($schemaFinder);
47 10
48 10
        foreach ($doctrineMap as $className => $doctrineMapping) {
49 10
            $this->mappings[$className] = [
50 10
                'doctrine'   => $doctrineMap[$className],
51 10
                'serializer' => isset($serializerMap[$className]) ? $serializerMap[$className] : null,
52 10
                'validation' => isset($validationMap[$className]) ? $validationMap[$className] : null,
53
                'schema' => isset($schemaMap[$className]) ? $schemaMap[$className] : null,
54 5
            ];
55 10
        }
56
    }
57
58
    /**
59
     * Get document
60
     *
61
     * @param string $className Document class
62
     * @return Document
63 10
     */
64
    public function getDocument($className)
65 10
    {
66 10
        if (isset($this->documents[$className])) {
67
            return $this->documents[$className];
68 10
        }
69
        if (!isset($this->mappings[$className])) {
70
            throw new \InvalidArgumentException(sprintf('No XML mapping found for document "%s"', $className));
71
        }
72 10
73 10
        return $this->documents[$className] = $this->processDocument(
74 10
            $className,
75 10
            $this->mappings[$className]['doctrine'],
76 10
            $this->mappings[$className]['serializer'],
77 10
            $this->mappings[$className]['validation'],
78 5
            $this->mappings[$className]['schema']
79
        );
80
    }
81
82
    /**
83
     * Get all documents
84
     *
85
     * @return Document[]
86 6
     */
87
    public function getDocuments()
88 6
    {
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 10
     */
103
    private function processDocument(
104
        $className,
105
        array $doctrineMapping,
106
        \DOMElement $serializerMapping = null,
107
        \DOMElement $validationMapping = null,
108
        array $schemaMapping = null
109 10
    ) {
110
        if ($serializerMapping === null) {
111
            $serializerFields = [];
112 10
        } else {
113 10
            $serializerFields = array_reduce(
114
                $this->getSerializerFields($serializerMapping),
115 10
                function (array $fields, array $field) {
116 10
                    $fields[$field['fieldName']] = $field;
117 10
                    return $fields;
118 10
                },
119 5
                []
120
            );
121
        }
122 10
123 2
        if ($validationMapping === null) {
124 1
            $validationFields = [];
125 10
        } else {
126 10
            $validationFields = array_reduce(
127
                $this->getValidationFields($validationMapping),
128 4
                function (array $fields, array $field) {
129 4
                    $fields[$field['fieldName']] = $field;
130 10
                    return $fields;
131 10
                },
132 5
                []
133
            );
134
        }
135 10
136 10
        if ($schemaMapping === null) {
137 5
            $schemaFields = [];
138 2
        } else {
139
            $schemaFields = $schemaMapping;
140
        }
141 10
142 10
        $fields = [];
143 10
        foreach ($this->getDoctrineFields($doctrineMapping) as $doctrineField) {
144 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
145 10
                $serializerFields[$doctrineField['name']] :
146 10
                null;
147 7
            $validationField = isset($validationFields[$doctrineField['name']]) ?
148 10
                $validationFields[$doctrineField['name']] :
149 10
                null;
150 6
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
151 10
                $schemaFields[$doctrineField['name']] :
152
                null;
153 10
154 2
            if ($doctrineField['type'] === 'collection') {
155 2
                $fields[] = new ArrayField(
156 2
                    $serializerField === null ? 'array<string>' : $serializerField['fieldType'],
157 2
                    $doctrineField['name'],
158 2
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
159 2
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
160 2
                    $validationField === null ? false : $validationField['required'],
161 2
                    $serializerField === null ? false : $serializerField['searchable'],
162 1
                    !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 1
                );
164 10
            } else {
165 10
                $fields[] = new Field(
166 10
                    $doctrineField['type'],
167 10
                    $doctrineField['name'],
168 10
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
169 10
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
170 10
                    $validationField === null ? false : $validationField['required'],
171 10
                    $serializerField === null ? false : $serializerField['searchable'],
172 5
                    !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
173
                );
174 5
            }
175 10
        }
176 10
        foreach ($this->getDoctrineEmbedOneFields($doctrineMapping) as $doctrineField) {
177 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
178 10
                $serializerFields[$doctrineField['name']] :
179 10
                null;
180 6
            $validationField = isset($validationFields[$doctrineField['name']]) ?
181 10
                $validationFields[$doctrineField['name']] :
182 10
                null;
183 6
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
184 10
                $schemaFields[$doctrineField['name']] :
185
                null;
186 10
187 10
            $fields[] = new EmbedOne(
188 10
                $this->getDocument($doctrineField['type']),
189 10
                $doctrineField['name'],
190 10
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
191 10
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
192 10
                $validationField === null ? false : $validationField['required'],
193 10
                $serializerField === null ? false : $serializerField['searchable'],
194 5
                !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 5
            );
196 10
        }
197 10
        foreach ($this->getDoctrineEmbedManyFields($doctrineMapping) as $doctrineField) {
198 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
199 10
                $serializerFields[$doctrineField['name']] :
200 10
                null;
201 5
            $validationField = isset($validationFields[$doctrineField['name']]) ?
202 10
                $validationFields[$doctrineField['name']] :
203 10
                null;
204 6
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
205 10
                $schemaFields[$doctrineField['name']] :
206
                null;
207 10
208 10
            $fields[] = new EmbedMany(
209 10
                $this->getDocument($doctrineField['type']),
210 10
                $doctrineField['name'],
211 10
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
212 10
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
213 10
                $validationField === null ? false : $validationField['required'],
214 5
                !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
215 5
            );
216
        }
217 10
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 10
     */
227
    private function loadDoctrineClassMap(Finder $finder)
228 10
    {
229 10
        $classMap = [];
230 10
        foreach ($finder as $file) {
231 10
            $classMap = array_merge(
232
                $classMap,
233 10
                Yaml::parseFile($file)
234 10
            );
235
        }
236 10
237 10
        // filter out superclasses
238
        $classMap = array_filter(
239 10
            $classMap,
240 10
            function ($classEntry) {
241 10
                return (!isset($classEntry['type']) || $classEntry['type'] != 'mappedSuperclass');
242 5
            }
243 5
        );
244 5
245
        return $classMap;
246 10
    }
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 10
    {
256
        $classMap = [];
257 10
        foreach ($finder as $file) {
258 10
            $document = new \DOMDocument();
259 10
            $document->load($file);
260 10
261
            $xpath = new \DOMXPath($document);
262 10
263
            $classMap = array_reduce(
264 10
                iterator_to_array($xpath->query('//class')),
265 10
                function (array $classMap, \DOMElement $element) {
266
                    $classMap[$element->getAttribute('name')] = $element;
267 10
                    return $classMap;
268 10
                },
269 10
                $classMap
270 5
            );
271 5
        }
272 5
273
        return $classMap;
274 10
    }
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 10
    {
284
        $classMap = [];
285 10
        foreach ($finder as $file) {
286 10
            $schema = json_decode(file_get_contents($file), true);
287 10
288
            if (!isset($schema['x-documentClass'])) {
289 10
                continue;
290 10
            }
291
292 View Code Duplication
            foreach ($schema['required'] as $field) {
293 2
                $classMap[$schema['x-documentClass']][$field]['required'] = true;
294 2
            }
295 1
            foreach ($schema['searchable'] as $field) {
296 2
                $classMap[$schema['x-documentClass']][$field]['searchable'] = 1;
297 2
            }
298 1 View Code Duplication
            foreach ($schema['readOnlyFields'] as $field) {
299 2
                $classMap[$schema['x-documentClass']][$field]['readOnly'] = true;
300 2
            }
301 1
302
            // flags from fields
303
            if (is_array($schema['properties'])) {
304 2
                foreach ($schema['properties'] as $fieldName => $field) {
305 2
                    if (isset($field['recordOriginException']) && $field['recordOriginException'] == true) {
306 2
                        $classMap[$schema['x-documentClass']][$fieldName]['recordOriginException'] = true;
307 2
                    }
308 1
                }
309 1
            }
310 1
        }
311 5
312
        return $classMap;
313 10
    }
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 10
    {
323
        $classMap = [];
324 10
        foreach ($finder as $file) {
325 10
            $document = new \DOMDocument();
326 10
            $document->load($file);
327 10
328
            $xpath = new \DOMXPath($document);
329 10
            $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
330 10
331
            $classMap = array_reduce(
332 10
                iterator_to_array($xpath->query('//constraint:class')),
333 10
                function (array $classMap, \DOMElement $element) {
334
                    $classMap[$element->getAttribute('name')] = $element;
335 10
                    return $classMap;
336 10
                },
337 10
                $classMap
338 5
            );
339 5
        }
340 5
341
        return $classMap;
342 10
    }
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 10
    {
352
        $xpath = new \DOMXPath($mapping->ownerDocument);
353 10
354
        return array_map(
355 10
            function (\DOMElement $element) {
356
                return [
357
                    'fieldName'   => $element->getAttribute('name'),
358 10
                    'fieldType'   => $this->getSerializerFieldType($element),
359 10
                    'exposedName' => $element->getAttribute('serialized-name') ?: $element->getAttribute('name'),
360 10
                    'readOnly'    => $element->getAttribute('read-only') === 'true',
361 10
                    'searchable'  => (int) $element->getAttribute('searchable')
362 10
                ];
363 5
            },
364 10
            iterator_to_array($xpath->query('property', $mapping))
365 10
        );
366 5
    }
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 10
    {
376
        if ($field->getAttribute('type')) {
377 10
            return $field->getAttribute('type');
378 10
        }
379
380
        $xpath = new \DOMXPath($field->ownerDocument);
381 2
382
        $type = $xpath->query('type', $field)->item(0);
383 2
        return $type === null ? null : $type->nodeValue;
384 2
    }
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 10
    {
394
        $xpath = new \DOMXPath($mapping->ownerDocument);
395 10
        $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
396 10
397
        return array_map(
398 10
            function (\DOMElement $element) use ($xpath) {
399
                $constraints = $xpath->query('constraint:constraint[@name="NotBlank" or @name="NotNull"]', $element);
400 4
                return [
401
                    'fieldName' => $element->getAttribute('name'),
402 4
                    'required'  => $constraints->length > 0,
403 4
                ];
404 2
            },
405 10
            iterator_to_array($xpath->query('constraint:property', $mapping))
406 10
        );
407 5
    }
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 10
    {
417
        if (!isset($mapping['fields'])) {
418 10
            return [];
419 10
        }
420
421 10
        return array_map(
422
            function ($key, $value) {
423
                if (!isset($value['type'])) {
424 10
                    $value['type'] = '';
425 10
                }
426 5
427 10
                return [
428 10
                    'name' => $key,
429 5
                    'type' => $value['type']
430
                ];
431
            },
432
            array_keys($mapping['fields']),
433
            $mapping['fields']
434
        );
435
    }
436
437
    /**
438 10
     * Get doctrine document embed-one fields
439
     *
440 10
     * @param array $mapping Doctrine mapping
441 10
     * @return array
442
     */
443 10
    private function getDoctrineEmbedOneFields(array $mapping)
444
    {
445
        return $this->getRelationList($mapping, 'One');
446 10
    }
447 10
448 5
    /**
449 10
     * Get doctrine document embed-many fields
450 10
     *
451 5
     * @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 10
     * gets list of relations
461
     *
462 10
     * @param array  $mapping mapping
463 10
     * @param string $suffix  suffix
464
     *
465 10
     * @return array relations
466 10
     */
467
    private function getRelationList($mapping, $suffix)
468 10
    {
469 10
        if (!isset($mapping['embed'.$suffix]) && !isset($mapping['reference'.$suffix])) {
470 5
            return [];
471 10
        }
472 10
473 5
        $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 4
            $relations
490
        );
491
    }
492
493
    /**
494
     * Gets an array of all fields, flat with full internal name in dot notation as key and
495 4
     * the exposed field name as value. You can pass a callable to limit the fields return a subset of fields.
496 4
     * If the callback returns true, the field will be included in the output. You will get the field definition
497 4
     * passed to your callback.
498 4
     *
499 2
     * @param Document $document       The document
500
     * @param string   $documentPrefix Document field prefix
501 4
     * @param string   $exposedPrefix  Exposed field prefix
502 2
     * @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 2
     *
504 2
     * @return array
505 1
     */
506 4
    public function getFieldNamesFlat(
507 4
        Document $document,
508 4
        $documentPrefix = '',
509 4
        $exposedPrefix = '',
510 4
        callable $callback = null
511 4
    ) {
512 4
        $result = [];
513 2
        foreach ($document->getFields() as $field) {
514 2 View Code Duplication
            if ($this->getFlatFieldCheckCallback($field, $callback)) {
515 2
                $result[$documentPrefix . $field->getFieldName()] = $exposedPrefix . $field->getExposedName();
516 4
            }
517 4
518 4
            if ($field instanceof ArrayField) {
519 4 View Code Duplication
                if ($this->getFlatFieldCheckCallback($field, $callback)) {
520 2
                    $result[$documentPrefix . $field->getFieldName() . '.0'] =
521 4
                        $exposedPrefix . $field->getExposedName() . '.0';
522 4
                }
523 4
            } elseif ($field instanceof EmbedOne) {
524 4
                $result = array_merge(
525 4
                    $result,
526 4
                    $this->getFieldNamesFlat(
527 2
                        $field->getDocument(),
528 2
                        $documentPrefix.$field->getFieldName().'.',
529 2
                        $exposedPrefix.$field->getExposedName().'.',
530 2
                        $callback
531 2
                    )
532
                );
533 4
            } 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 4
                    )
546
                );
547 4
            }
548 4
        }
549
550
        return $result;
551 2
    }
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