DocumentMap::getDoctrineFields()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

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