Completed
Pull Request — develop (#640)
by Narcotic
08:00 queued 01:02
created

DocumentMap::getDocument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0052

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
ccs 11
cts 12
cp 0.9167
cc 3
eloc 11
nc 3
nop 1
crap 3.0052
1
<?php
2
/**
3
 * DocumentMap class file
4
 */
5
6
namespace Graviton\DocumentBundle\DependencyInjection\Compiler\Utils;
7
8
use Symfony\Component\Finder\Finder;
9
10
/**
11
 * Document map
12
 *
13
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
14
 * @license  https://opensource.org/licenses/MIT MIT License
15
 * @link     http://swisscom.ch
16
 */
17
class DocumentMap
18
{
19
    /**
20
     * @var array
21
     */
22
    private $mappings = [];
23
    /**
24
     * @var Document[]
25
     */
26
    private $documents = [];
27
28
    /**
29
     * Constructor
30
     *
31
     * @param Finder $doctrineFinder   Doctrine mapping finder
32
     * @param Finder $serializerFinder Serializer mapping finder
33
     * @param Finder $validationFinder Validation mapping finder
34
     * @param Finder $schemaFinder     Schema finder
35
     */
36 10
    public function __construct(
37
        Finder $doctrineFinder,
38
        Finder $serializerFinder,
39
        Finder $validationFinder,
40
        Finder $schemaFinder
41
    ) {
42 10
        $doctrineMap = $this->loadDoctrineClassMap($doctrineFinder);
43 10
        $serializerMap = $this->loadSerializerClassMap($serializerFinder);
44 10
        $validationMap = $this->loadValidationClassMap($validationFinder);
45 10
        $schemaMap = $this->loadSchemaClassMap($schemaFinder);
46
47 10
        foreach ($doctrineMap as $className => $doctrineMapping) {
48 10
            $this->mappings[$className] = [
49 10
                'doctrine'   => $doctrineMap[$className],
50 10
                'serializer' => isset($serializerMap[$className]) ? $serializerMap[$className] : null,
51 10
                'validation' => isset($validationMap[$className]) ? $validationMap[$className] : null,
52 10
                'schema' => isset($schemaMap[$className]) ? $schemaMap[$className] : null,
53
            ];
54 5
        }
55 10
    }
56
57
    /**
58
     * Get document
59
     *
60
     * @param string $className Document class
61
     * @return Document
62
     */
63 10
    public function getDocument($className)
64
    {
65 10
        if (isset($this->documents[$className])) {
66 10
            return $this->documents[$className];
67
        }
68 10
        if (!isset($this->mappings[$className])) {
69
            throw new \InvalidArgumentException(sprintf('No XML mapping found for document "%s"', $className));
70
        }
71
72 10
        return $this->documents[$className] = $this->processDocument(
73 10
            $className,
74 10
            $this->mappings[$className]['doctrine'],
75 10
            $this->mappings[$className]['serializer'],
76 10
            $this->mappings[$className]['validation'],
77 10
            $this->mappings[$className]['schema']
78 5
        );
79
    }
80
81
    /**
82
     * Get all documents
83
     *
84
     * @return Document[]
85
     */
86 6
    public function getDocuments()
87
    {
88 6
        return array_map([$this, 'getDocument'], array_keys($this->mappings));
89
    }
90
91
    /**
92
     * Process document
93
     *
94
     * @param string      $className         Class name
95
     * @param \DOMElement $doctrineMapping   Doctrine XML mapping
96
     * @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...
97
     * @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...
98
     * @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...
99
     *
100
     * @return Document
101
     */
102 10
    private function processDocument(
103
        $className,
104
        \DOMElement $doctrineMapping,
105
        \DOMElement $serializerMapping = null,
106
        \DOMElement $validationMapping = null,
107
        array $schemaMapping = null
108
    ) {
109 10
        if ($serializerMapping === null) {
110
            $serializerFields = [];
111
        } else {
112 10
            $serializerFields = array_reduce(
113 10
                $this->getSerializerFields($serializerMapping),
114
                function (array $fields, array $field) {
115 10
                    $fields[$field['fieldName']] = $field;
116 10
                    return $fields;
117 10
                },
118 10
                []
119 5
            );
120
        }
121
122 10
        if ($validationMapping === null) {
123 2
            $validationFields = [];
124 1
        } else {
125 10
            $validationFields = array_reduce(
126 10
                $this->getValidationFields($validationMapping),
127
                function (array $fields, array $field) {
128 4
                    $fields[$field['fieldName']] = $field;
129 4
                    return $fields;
130 10
                },
131 10
                []
132 5
            );
133
        }
134
135 10
        if ($schemaMapping === null) {
136 10
            $schemaFields = [];
137 5
        } else {
138 2
            $schemaFields = $schemaMapping;
139
        }
140
141 10
        $fields = [];
142 10
        foreach ($this->getDoctrineFields($doctrineMapping) as $doctrineField) {
143 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
144 10
                $serializerFields[$doctrineField['name']] :
145 10
                null;
146 10
            $validationField = isset($validationFields[$doctrineField['name']]) ?
147 7
                $validationFields[$doctrineField['name']] :
148 10
                null;
149 10
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
150 6
                $schemaFields[$doctrineField['name']] :
151 10
                null;
152
153 10
            if ($doctrineField['type'] === 'collection') {
154 2
                $fields[] = new ArrayField(
155 2
                    $serializerField === null ? 'array<string>' : $serializerField['fieldType'],
156 2
                    $doctrineField['name'],
157 2
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
158 2
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
159 2
                    $validationField === null ? false : $validationField['required'],
160 2
                    $serializerField === null ? false : $serializerField['searchable'],
161 2
                    !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...
162 1
                );
163 1
            } else {
164 10
                $fields[] = new Field(
165 10
                    $doctrineField['type'],
166 10
                    $doctrineField['name'],
167 10
                    $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
168 10
                    !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
169 10
                    $validationField === null ? false : $validationField['required'],
170 10
                    $serializerField === null ? false : $serializerField['searchable'],
171 10
                    !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
172 5
                );
173
            }
174 5
        }
175 10
        foreach ($this->getDoctrineEmbedOneFields($doctrineMapping) as $doctrineField) {
176 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
177 10
                $serializerFields[$doctrineField['name']] :
178 10
                null;
179 10
            $validationField = isset($validationFields[$doctrineField['name']]) ?
180 6
                $validationFields[$doctrineField['name']] :
181 10
                null;
182 10
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
183 6
                $schemaFields[$doctrineField['name']] :
184 10
                null;
185
186 10
            $fields[] = new EmbedOne(
187 10
                $this->getDocument($doctrineField['type']),
188 10
                $doctrineField['name'],
189 10
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
190 10
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
191 10
                $validationField === null ? false : $validationField['required'],
192 10
                $serializerField === null ? false : $serializerField['searchable'],
193 10
                !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...
194 5
            );
195 5
        }
196 10
        foreach ($this->getDoctrineEmbedManyFields($doctrineMapping) as $doctrineField) {
197 10
            $serializerField = isset($serializerFields[$doctrineField['name']]) ?
198 10
                $serializerFields[$doctrineField['name']] :
199 10
                null;
200 10
            $validationField = isset($validationFields[$doctrineField['name']]) ?
201 5
                $validationFields[$doctrineField['name']] :
202 10
                null;
203 10
            $schemaField = isset($schemaFields[$doctrineField['name']]) ?
204 6
                $schemaFields[$doctrineField['name']] :
205 10
                null;
206
207 10
            $fields[] = new EmbedMany(
208 10
                $this->getDocument($doctrineField['type']),
209 10
                $doctrineField['name'],
210 10
                $serializerField === null ? $doctrineField['name'] : $serializerField['exposedName'],
211 10
                !isset($schemaField['readOnly']) ? false : $schemaField['readOnly'],
212 10
                $validationField === null ? false : $validationField['required'],
213 10
                !isset($schemaField['recordOriginException']) ? false : $schemaField['recordOriginException']
214 5
            );
215 5
        }
216
217 10
        return new Document($className, $fields);
218
    }
219
220
    /**
221
     * Load doctrine class map
222
     *
223
     * @param Finder $finder Mapping finder
224
     * @return array
225
     */
226 10 View Code Duplication
    private function loadDoctrineClassMap(Finder $finder)
227
    {
228 10
        $classMap = [];
229 10
        foreach ($finder as $file) {
230 10
            $document = new \DOMDocument();
231 10
            $document->load($file);
232
233 10
            $xpath = new \DOMXPath($document);
234 10
            $xpath->registerNamespace('doctrine', 'http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping');
235
236 10
            $classMap = array_reduce(
237 10
                iterator_to_array($xpath->query('//*[self::doctrine:document or self::doctrine:embedded-document]')),
238
                function (array $classMap, \DOMElement $element) {
239 10
                    $classMap[$element->getAttribute('name')] = $element;
240 10
                    return $classMap;
241 10
                },
242 5
                $classMap
243 5
            );
244 5
        }
245
246 10
        return $classMap;
247
    }
248
249
    /**
250
     * Load serializer class map
251
     *
252
     * @param Finder $finder Mapping finder
253
     * @return array
254
     */
255 10 View Code Duplication
    private function loadSerializerClassMap(Finder $finder)
256
    {
257 10
        $classMap = [];
258 10
        foreach ($finder as $file) {
259 10
            $document = new \DOMDocument();
260 10
            $document->load($file);
261
262 10
            $xpath = new \DOMXPath($document);
263
264 10
            $classMap = array_reduce(
265 10
                iterator_to_array($xpath->query('//class')),
266
                function (array $classMap, \DOMElement $element) {
267 10
                    $classMap[$element->getAttribute('name')] = $element;
268 10
                    return $classMap;
269 10
                },
270 5
                $classMap
271 5
            );
272 5
        }
273
274 10
        return $classMap;
275
    }
276
277
    /**
278
     * Load schema class map
279
     *
280
     * @param Finder $finder Mapping finder
281
     * @return array
282
     */
283 10
    private function loadSchemaClassMap(Finder $finder)
284
    {
285 10
        $classMap = [];
286 10
        foreach ($finder as $file) {
287 10
            $schema = json_decode(file_get_contents($file), true);
288
289 10
            if (!isset($schema['x-documentClass'])) {
290 10
                continue;
291
            }
292
293 2 View Code Duplication
            foreach ($schema['required'] as $field) {
294 2
                $classMap[$schema['x-documentClass']][$field]['required'] = true;
295 1
            }
296 2
            foreach ($schema['searchable'] as $field) {
297 2
                $classMap[$schema['x-documentClass']][$field]['searchable'] = 1;
298 1
            }
299 2 View Code Duplication
            foreach ($schema['readOnlyFields'] as $field) {
300 2
                $classMap[$schema['x-documentClass']][$field]['readOnly'] = true;
301 1
            }
302
303
            // flags from fields
304 2
            if (is_array($schema['properties'])) {
305 2
                foreach ($schema['properties'] as $fieldName => $field) {
306 2
                    if (isset($field['recordOriginException']) && $field['recordOriginException'] == true) {
307 2
                        $classMap[$schema['x-documentClass']][$fieldName]['recordOriginException'] = true;
308 1
                    }
309 1
                }
310 1
            }
311 5
        }
312
313 10
        return $classMap;
314
    }
315
316
    /**
317
     * Load validation class map
318
     *
319
     * @param Finder $finder Mapping finder
320
     * @return array
321
     */
322 10 View Code Duplication
    private function loadValidationClassMap(Finder $finder)
323
    {
324 10
        $classMap = [];
325 10
        foreach ($finder as $file) {
326 10
            $document = new \DOMDocument();
327 10
            $document->load($file);
328
329 10
            $xpath = new \DOMXPath($document);
330 10
            $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
331
332 10
            $classMap = array_reduce(
333 10
                iterator_to_array($xpath->query('//constraint:class')),
334
                function (array $classMap, \DOMElement $element) {
335 10
                    $classMap[$element->getAttribute('name')] = $element;
336 10
                    return $classMap;
337 10
                },
338 5
                $classMap
339 5
            );
340 5
        }
341
342 10
        return $classMap;
343
    }
344
345
    /**
346
     * Get serializer fields
347
     *
348
     * @param \DOMElement $mapping Serializer XML mapping
349
     * @return array
350
     */
351 10
    private function getSerializerFields(\DOMElement $mapping)
352
    {
353 10
        $xpath = new \DOMXPath($mapping->ownerDocument);
354
355 10
        return array_map(
356
            function (\DOMElement $element) {
357
                return [
358 10
                    'fieldName'   => $element->getAttribute('name'),
359 10
                    'fieldType'   => $this->getSerializerFieldType($element),
360 10
                    'exposedName' => $element->getAttribute('serialized-name') ?: $element->getAttribute('name'),
361 10
                    'readOnly'    => $element->getAttribute('read-only') === 'true',
362 10
                    'searchable'  => (int) $element->getAttribute('searchable')
363 5
                ];
364 10
            },
365 10
            iterator_to_array($xpath->query('property', $mapping))
366 5
        );
367
    }
368
369
    /**
370
     * Get serializer field type
371
     *
372
     * @param \DOMElement $field Field node
373
     * @return string|null
374
     */
375 10
    private function getSerializerFieldType(\DOMElement $field)
376
    {
377 10
        if ($field->getAttribute('type')) {
378 10
            return $field->getAttribute('type');
379
        }
380
381 2
        $xpath = new \DOMXPath($field->ownerDocument);
382
383 2
        $type = $xpath->query('type', $field)->item(0);
384 2
        return $type === null ? null : $type->nodeValue;
385
    }
386
387
    /**
388
     * Get validation fields
389
     *
390
     * @param \DOMElement $mapping Validation XML mapping
391
     * @return array
392
     */
393 10
    private function getValidationFields(\DOMElement $mapping)
394
    {
395 10
        $xpath = new \DOMXPath($mapping->ownerDocument);
396 10
        $xpath->registerNamespace('constraint', 'http://symfony.com/schema/dic/constraint-mapping');
397
398 10
        return array_map(
399
            function (\DOMElement $element) use ($xpath) {
400 4
                $constraints = $xpath->query('constraint:constraint[@name="NotBlank" or @name="NotNull"]', $element);
401
                return [
402 4
                    'fieldName' => $element->getAttribute('name'),
403 4
                    'required'  => $constraints->length > 0,
404 2
                ];
405 10
            },
406 10
            iterator_to_array($xpath->query('constraint:property', $mapping))
407 5
        );
408
    }
409
410
    /**
411
     * Get doctrine document fields
412
     *
413
     * @param \DOMElement $mapping Doctrine XML mapping
414
     * @return array
415
     */
416 10 View Code Duplication
    private function getDoctrineFields(\DOMElement $mapping)
417
    {
418 10
        $xpath = new \DOMXPath($mapping->ownerDocument);
419 10
        $xpath->registerNamespace('doctrine', 'http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping');
420
421 10
        return array_map(
422
            function (\DOMElement $element) {
423
                return [
424 10
                    'name' => $element->getAttribute('fieldName'),
425 10
                    'type' => $element->getAttribute('type'),
426 5
                ];
427 10
            },
428 10
            iterator_to_array($xpath->query('doctrine:field', $mapping))
429 5
        );
430
    }
431
432
    /**
433
     * Get doctrine document embed-one fields
434
     *
435
     * @param \DOMElement $mapping Doctrine XML mapping
436
     * @return array
437
     */
438 10 View Code Duplication
    private function getDoctrineEmbedOneFields(\DOMElement $mapping)
439
    {
440 10
        $xpath = new \DOMXPath($mapping->ownerDocument);
441 10
        $xpath->registerNamespace('doctrine', 'http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping');
442
443 10
        return array_map(
444
            function (\DOMElement $element) {
445
                return [
446 10
                    'name' => $element->getAttribute('field'),
447 10
                    'type' => $element->getAttribute('target-document'),
448 5
                ];
449 10
            },
450 10
            iterator_to_array($xpath->query('*[self::doctrine:embed-one or self::doctrine:reference-one]', $mapping))
451 5
        );
452
    }
453
454
    /**
455
     * Get doctrine document embed-many fields
456
     *
457
     * @param \DOMElement $mapping Doctrine XML mapping
458
     * @return array
459
     */
460 10 View Code Duplication
    private function getDoctrineEmbedManyFields(\DOMElement $mapping)
461
    {
462 10
        $xpath = new \DOMXPath($mapping->ownerDocument);
463 10
        $xpath->registerNamespace('doctrine', 'http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping');
464
465 10
        return array_map(
466 10
            function (\DOMElement $element) {
467
                return [
468 10
                    'name' => $element->getAttribute('field'),
469 10
                    'type' => $element->getAttribute('target-document'),
470 5
                ];
471 10
            },
472 10
            iterator_to_array($xpath->query('*[self::doctrine:embed-many or self::doctrine:reference-many]', $mapping))
473 5
        );
474
    }
475
476
    /**
477
     * Gets an array of all fields, flat with full internal name in dot notation as key and
478
     * the exposed field name as value. You can pass a callable to limit the fields return a subset of fields.
479
     * If the callback returns true, the field will be included in the output. You will get the field definition
480
     * passed to your callback.
481
     *
482
     * @param Document $document       The document
483
     * @param string   $documentPrefix Document field prefix
484
     * @param string   $exposedPrefix  Exposed field prefix
485
     * @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...
486
     *
487
     * @return array
488
     */
489 4
    public function getFieldNamesFlat(
490
        Document $document,
491
        $documentPrefix = '',
492
        $exposedPrefix = '',
493
        callable $callback = null
494
    ) {
495 4
        $result = [];
496 4
        foreach ($document->getFields() as $field) {
497 4 View Code Duplication
            if ($this->getFlatFieldCheckCallback($field, $callback)) {
498 4
                $result[$documentPrefix . $field->getFieldName()] = $exposedPrefix . $field->getExposedName();
499 2
            }
500
501 4
            if ($field instanceof ArrayField) {
502 2 View Code Duplication
                if ($this->getFlatFieldCheckCallback($field, $callback)) {
503 2
                    $result[$documentPrefix . $field->getFieldName() . '.0'] =
504 2
                        $exposedPrefix . $field->getExposedName() . '.0';
505 1
                }
506 4
            } elseif ($field instanceof EmbedOne) {
507 4
                $result = array_merge(
508 4
                    $result,
509 4
                    $this->getFieldNamesFlat(
510 4
                        $field->getDocument(),
511 4
                        $documentPrefix.$field->getFieldName().'.',
512 4
                        $exposedPrefix.$field->getExposedName().'.',
513 2
                        $callback
514 2
                    )
515 2
                );
516 4
            } elseif ($field instanceof EmbedMany) {
517 4 View Code Duplication
                if ($this->getFlatFieldCheckCallback($field, $callback)) {
518 4
                    $result[$documentPrefix . $field->getFieldName() . '.0'] =
519 4
                        $exposedPrefix . $field->getExposedName() . '.0';
520 2
                }
521 4
                $result = array_merge(
522 4
                    $result,
523 4
                    $this->getFieldNamesFlat(
524 4
                        $field->getDocument(),
525 4
                        $documentPrefix.$field->getFieldName().'.0.',
526 4
                        $exposedPrefix.$field->getExposedName().'.0.',
527 2
                        $callback
528 2
                    )
529 2
                );
530 2
            }
531 2
        }
532
533 4
        return $result;
534
    }
535
536
    /**
537
     * Simple function to check whether a given shall be returned in the output of getFieldNamesFlat
538
     * and the optional given callback there.
539
     *
540
     * @param AbstractField $field    field
541
     * @param callable|null $callback optional callback
542
     *
543
     * @return bool|mixed true if field should be returned, false otherwise
544
     */
545 4
    private function getFlatFieldCheckCallback($field, callable $callback = null)
546
    {
547 4
        if (!is_callable($callback)) {
548 4
            return true;
549
        }
550
551 2
        return call_user_func($callback, $field);
552
    }
553
}
554