Completed
Push — master ( a8fe50...bce26f )
by Maciej
13s
created

Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php (13 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Mapping\Driver;
6
7
use Doctrine\Common\Persistence\Mapping\Driver\FileDriver;
8
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
9
use Doctrine\ODM\MongoDB\Mapping\MappingException;
10
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
11
use DOMDocument;
12
use LibXMLError;
13
use function array_keys;
14
use function array_map;
15
use function constant;
16
use function count;
17
use function current;
18
use function explode;
19
use function implode;
20
use function in_array;
21
use function is_numeric;
22
use function iterator_to_array;
23
use function libxml_clear_errors;
24
use function libxml_get_errors;
25
use function libxml_use_internal_errors;
26
use function next;
27
use function preg_match;
28
use function simplexml_load_file;
29
use function sprintf;
30
use function strtoupper;
31
use function trim;
32
33
/**
34
 * XmlDriver is a metadata driver that enables mapping through XML files.
35
 *
36
 */
37
class XmlDriver extends FileDriver
38
{
39
    public const DEFAULT_FILE_EXTENSION = '.dcm.xml';
40
41
    private const DEFAULT_GRIDFS_MAPPINGS = [
42
        'length' => [
43
            'name' => 'length',
44
            'type' => 'int',
45
            'notSaved' => true,
46
        ],
47
        'chunk-size' => [
48
            'name' => 'chunkSize',
49
            'type' => 'int',
50
            'notSaved' => true,
51
        ],
52
        'filename' => [
53
            'name' => 'filename',
54
            'type' => 'string',
55
            'notSaved' => true,
56
        ],
57
        'upload-date' => [
58
            'name' => 'uploadDate',
59
            'type' => 'date',
60
            'notSaved' => true,
61
        ],
62
    ];
63
64
    /**
65
     * {@inheritDoc}
66
     */
67 15
    public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENSION)
68
    {
69 15
        parent::__construct($locator, $fileExtension);
70 15
    }
71
72
    /**
73
     * {@inheritDoc}
74
     */
75 9
    public function loadMetadataForClass($className, \Doctrine\Common\Persistence\Mapping\ClassMetadata $class)
76
    {
77
        /** @var ClassMetadata $class */
78
        /** @var \SimpleXMLElement $xmlRoot */
79 9
        $xmlRoot = $this->getElement($className);
80 7
        if (! $xmlRoot) {
81
            return;
82
        }
83
84 7
        if ($xmlRoot->getName() === 'document') {
85 6
            if (isset($xmlRoot['repository-class'])) {
86 6
                $class->setCustomRepositoryClass((string) $xmlRoot['repository-class']);
87
            }
88 3
        } elseif ($xmlRoot->getName() === 'mapped-superclass') {
89 1
            $class->setCustomRepositoryClass(
90 1
                isset($xmlRoot['repository-class']) ? (string) $xmlRoot['repository-class'] : null
91
            );
92 1
            $class->isMappedSuperclass = true;
0 ignored issues
show
Accessing isMappedSuperclass on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
93 2
        } elseif ($xmlRoot->getName() === 'embedded-document') {
94 1
            $class->isEmbeddedDocument = true;
0 ignored issues
show
Accessing isEmbeddedDocument on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
95 2
        } elseif ($xmlRoot->getName() === 'query-result-document') {
96 1
            $class->isQueryResultDocument = true;
0 ignored issues
show
Accessing isQueryResultDocument on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
97 1
        } elseif ($xmlRoot->getName() === 'gridfs-file') {
98 1
            $class->isFile = true;
0 ignored issues
show
Accessing isFile on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
99
100 1
            if (isset($xmlRoot['chunk-size-bytes'])) {
101 1
                $class->setChunkSizeBytes((int) $xmlRoot['chunk-size-bytes']);
102
            }
103
        }
104
105 7
        if (isset($xmlRoot['db'])) {
106 3
            $class->setDatabase((string) $xmlRoot['db']);
107
        }
108
109 7
        if (isset($xmlRoot['collection'])) {
110 5
            if (isset($xmlRoot['capped-collection'])) {
111
                $config = ['name' => (string) $xmlRoot['collection']];
112
                $config['capped'] = (bool) $xmlRoot['capped-collection'];
113
                if (isset($xmlRoot['capped-collection-max'])) {
114
                    $config['max'] = (int) $xmlRoot['capped-collection-max'];
115
                }
116
                if (isset($xmlRoot['capped-collection-size'])) {
117
                    $config['size'] = (int) $xmlRoot['capped-collection-size'];
118
                }
119
                $class->setCollection($config);
120
            } else {
121 5
                $class->setCollection((string) $xmlRoot['collection']);
122
            }
123
        }
124 7
        if (isset($xmlRoot['bucket-name'])) {
125
            $class->setBucketName((string) $xmlRoot['bucket-name']);
126
        }
127 7
        if (isset($xmlRoot['write-concern'])) {
128
            $class->setWriteConcern((string) $xmlRoot['write-concern']);
129
        }
130 7
        if (isset($xmlRoot['inheritance-type'])) {
131
            $inheritanceType = (string) $xmlRoot['inheritance-type'];
132
            $class->setInheritanceType(constant(ClassMetadata::class . '::INHERITANCE_TYPE_' . $inheritanceType));
133
        }
134 7
        if (isset($xmlRoot['change-tracking-policy'])) {
135 1
            $class->setChangeTrackingPolicy(constant(ClassMetadata::class . '::CHANGETRACKING_' . strtoupper((string) $xmlRoot['change-tracking-policy'])));
136
        }
137 7
        if (isset($xmlRoot->{'discriminator-field'})) {
138
            $discrField = $xmlRoot->{'discriminator-field'};
139
            $class->setDiscriminatorField((string) $discrField['name']);
140
        }
141 7
        if (isset($xmlRoot->{'discriminator-map'})) {
142
            $map = [];
143
            foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
144
                $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
145
            }
146
            $class->setDiscriminatorMap($map);
147
        }
148 7
        if (isset($xmlRoot->{'default-discriminator-value'})) {
149
            $class->setDefaultDiscriminatorValue((string) $xmlRoot->{'default-discriminator-value'}['value']);
150
        }
151 7
        if (isset($xmlRoot->{'indexes'})) {
152 1
            foreach ($xmlRoot->{'indexes'}->{'index'} as $index) {
153 1
                $this->addIndex($class, $index);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
154
            }
155
        }
156 7
        if (isset($xmlRoot->{'shard-key'})) {
157
            $this->setShardKey($class, $xmlRoot->{'shard-key'}[0]);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
158
        }
159 7
        if (isset($xmlRoot['read-only']) && (string) $xmlRoot['read-only'] === 'true') {
160
            $class->markReadOnly();
161
        }
162 7
        if (isset($xmlRoot->{'read-preference'})) {
163
            $class->setReadPreference(...$this->transformReadPreference($xmlRoot->{'read-preference'}));
164
        }
165
166 7
        if (isset($xmlRoot->id)) {
167 7
            $field = $xmlRoot->id;
168
            $mapping = [
169 7
                'id' => true,
170
                'fieldName' => 'id',
171
            ];
172
173 7
            $attributes = $field->attributes();
174 7
            foreach ($attributes as $key => $value) {
175 2
                $mapping[$key] = (string) $value;
176
            }
177
178 7
            if (isset($mapping['strategy'])) {
179 2
                $mapping['options'] = [];
180 2
                if (isset($field->{'generator-option'})) {
181 1
                    foreach ($field->{'generator-option'} as $generatorOptions) {
182 1
                        $attributesGenerator = iterator_to_array($generatorOptions->attributes());
183 1
                        if (! isset($attributesGenerator['name']) || ! isset($attributesGenerator['value'])) {
184
                            continue;
185
                        }
186
187 1
                        $mapping['options'][(string) $attributesGenerator['name']] = (string) $attributesGenerator['value'];
188
                    }
189
                }
190
            }
191
192 7
            $this->addFieldMapping($class, $mapping);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
193
        }
194
195 7
        if (isset($xmlRoot->field)) {
196 2
            foreach ($xmlRoot->field as $field) {
197 2
                $mapping = [];
198 2
                $attributes = $field->attributes();
199 2
                foreach ($attributes as $key => $value) {
200 2
                    $mapping[$key] = (string) $value;
201 2
                    $booleanAttributes = ['reference', 'embed', 'unique', 'sparse'];
202 2
                    if (! in_array($key, $booleanAttributes)) {
203 2
                        continue;
204
                    }
205
206 1
                    $mapping[$key] = ($mapping[$key] === 'true');
207
                }
208
209 2
                if (isset($attributes['not-saved'])) {
210
                    $mapping['notSaved'] = ((string) $attributes['not-saved'] === 'true');
211
                }
212
213 2
                if (isset($attributes['field-name'])) {
214
                    $mapping['fieldName'] = (string) $attributes['field-name'];
215
                }
216
217 2
                if (isset($attributes['also-load'])) {
218
                    $mapping['alsoLoadFields'] = explode(',', $attributes['also-load']);
219 2
                } elseif (isset($attributes['version'])) {
220
                    $mapping['version'] = ((string) $attributes['version'] === 'true');
221 2
                } elseif (isset($attributes['lock'])) {
222
                    $mapping['lock'] = ((string) $attributes['lock'] === 'true');
223
                }
224
225 2
                $this->addFieldMapping($class, $mapping);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
226
            }
227
        }
228
229 7
        $this->addGridFSMappings($class, $xmlRoot);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
230
231 7
        if (isset($xmlRoot->{'embed-one'})) {
232 1
            foreach ($xmlRoot->{'embed-one'} as $embed) {
233 1
                $this->addEmbedMapping($class, $embed, 'one');
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
234
            }
235
        }
236 7
        if (isset($xmlRoot->{'embed-many'})) {
237 1
            foreach ($xmlRoot->{'embed-many'} as $embed) {
238 1
                $this->addEmbedMapping($class, $embed, 'many');
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
239
            }
240
        }
241 7
        if (isset($xmlRoot->{'reference-many'})) {
242 3
            foreach ($xmlRoot->{'reference-many'} as $reference) {
243 3
                $this->addReferenceMapping($class, $reference, 'many');
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
244
            }
245
        }
246 7
        if (isset($xmlRoot->{'reference-one'})) {
247 2
            foreach ($xmlRoot->{'reference-one'} as $reference) {
248 2
                $this->addReferenceMapping($class, $reference, 'one');
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
249
            }
250
        }
251 7
        if (isset($xmlRoot->{'lifecycle-callbacks'})) {
252 1
            foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) {
253 1
                $class->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ODM\MongoDB\Events::' . (string) $lifecycleCallback['type']));
254
            }
255
        }
256 7
        if (! isset($xmlRoot->{'also-load-methods'})) {
257 7
            return;
258
        }
259
260 1
        foreach ($xmlRoot->{'also-load-methods'}->{'also-load-method'} as $alsoLoadMethod) {
261 1
            $class->registerAlsoLoadMethod((string) $alsoLoadMethod['method'], (string) $alsoLoadMethod['field']);
262
        }
263 1
    }
264
265 8
    private function addFieldMapping(ClassMetadata $class, array $mapping): void
266
    {
267 8
        if (isset($mapping['name'])) {
268 6
            $name = $mapping['name'];
269 7
        } elseif (isset($mapping['fieldName'])) {
270 7
            $name = $mapping['fieldName'];
271
        } else {
272
            throw new \InvalidArgumentException('Cannot infer a MongoDB name from the mapping');
273
        }
274
275 8
        $class->mapField($mapping);
276
277
        // Index this field if either "index", "unique", or "sparse" are set
278 8
        if (! (isset($mapping['index']) || isset($mapping['unique']) || isset($mapping['sparse']))) {
279 8
            return;
280
        }
281
282 1
        $keys = [$name => $mapping['order'] ?? 'asc'];
283 1
        $options = [];
284
285 1
        if (isset($mapping['background'])) {
286
            $options['background'] = (bool) $mapping['background'];
287
        }
288 1
        if (isset($mapping['drop-dups'])) {
289
            $options['dropDups'] = (bool) $mapping['drop-dups'];
290
        }
291 1
        if (isset($mapping['index-name'])) {
292
            $options['name'] = (string) $mapping['index-name'];
293
        }
294 1
        if (isset($mapping['sparse'])) {
295 1
            $options['sparse'] = (bool) $mapping['sparse'];
296
        }
297 1
        if (isset($mapping['unique'])) {
298 1
            $options['unique'] = (bool) $mapping['unique'];
299
        }
300
301 1
        $class->addIndex($keys, $options);
302 1
    }
303
304 2
    private function addEmbedMapping(ClassMetadata $class, \SimpleXmlElement $embed, string $type): void
305
    {
306 2
        $attributes = $embed->attributes();
307 2
        $defaultStrategy = $type === 'one' ? ClassMetadata::STORAGE_STRATEGY_SET : CollectionHelper::DEFAULT_STRATEGY;
308
        $mapping = [
309 2
            'type'            => $type,
310
            'embedded'        => true,
311 2
            'targetDocument'  => isset($attributes['target-document']) ? (string) $attributes['target-document'] : null,
312 2
            'collectionClass' => isset($attributes['collection-class']) ? (string) $attributes['collection-class'] : null,
313 2
            'name'            => (string) $attributes['field'],
314 2
            'strategy'        => (string) ($attributes['strategy'] ?? $defaultStrategy),
315
        ];
316 2
        if (isset($attributes['field-name'])) {
317
            $mapping['fieldName'] = (string) $attributes['field-name'];
318
        }
319 2
        if (isset($embed->{'discriminator-field'})) {
320
            $attr = $embed->{'discriminator-field'};
321
            $mapping['discriminatorField'] = (string) $attr['name'];
322
        }
323 2
        if (isset($embed->{'discriminator-map'})) {
324
            foreach ($embed->{'discriminator-map'}->{'discriminator-mapping'} as $discriminatorMapping) {
325
                $attr = $discriminatorMapping->attributes();
326
                $mapping['discriminatorMap'][(string) $attr['value']] = (string) $attr['class'];
327
            }
328
        }
329 2
        if (isset($embed->{'default-discriminator-value'})) {
330
            $mapping['defaultDiscriminatorValue'] = (string) $embed->{'default-discriminator-value'}['value'];
331
        }
332 2
        if (isset($attributes['not-saved'])) {
333
            $mapping['notSaved'] = ((string) $attributes['not-saved'] === 'true');
334
        }
335 2
        if (isset($attributes['also-load'])) {
336
            $mapping['alsoLoadFields'] = explode(',', $attributes['also-load']);
337
        }
338 2
        $this->addFieldMapping($class, $mapping);
339 2
    }
340
341 4
    private function addReferenceMapping(ClassMetadata $class, $reference, string $type): void
342
    {
343 4
        $cascade = array_keys((array) $reference->cascade);
344 4
        if (count($cascade) === 1) {
345 1
            $cascade = current($cascade) ?: next($cascade);
346
        }
347 4
        $attributes = $reference->attributes();
348 4
        $defaultStrategy = $type === 'one' ? ClassMetadata::STORAGE_STRATEGY_SET : CollectionHelper::DEFAULT_STRATEGY;
349
        $mapping = [
350 4
            'cascade'          => $cascade,
351 4
            'orphanRemoval'    => isset($attributes['orphan-removal']) ? ((string) $attributes['orphan-removal'] === 'true') : false,
352 4
            'type'             => $type,
353
            'reference'        => true,
354 4
            'storeAs'          => (string) ($attributes['store-as'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF),
355 4
            'targetDocument'   => isset($attributes['target-document']) ? (string) $attributes['target-document'] : null,
356 4
            'collectionClass'  => isset($attributes['collection-class']) ? (string) $attributes['collection-class'] : null,
357 4
            'name'             => (string) $attributes['field'],
358 4
            'strategy'         => (string) ($attributes['strategy'] ?? $defaultStrategy),
359 4
            'inversedBy'       => isset($attributes['inversed-by']) ? (string) $attributes['inversed-by'] : null,
360 4
            'mappedBy'         => isset($attributes['mapped-by']) ? (string) $attributes['mapped-by'] : null,
361 4
            'repositoryMethod' => isset($attributes['repository-method']) ? (string) $attributes['repository-method'] : null,
362 4
            'limit'            => isset($attributes['limit']) ? (int) $attributes['limit'] : null,
363 4
            'skip'             => isset($attributes['skip']) ? (int) $attributes['skip'] : null,
364
            'prime'            => [],
365
        ];
366
367 4
        if (isset($attributes['field-name'])) {
368
            $mapping['fieldName'] = (string) $attributes['field-name'];
369
        }
370 4
        if (isset($reference->{'discriminator-field'})) {
371
            $attr = $reference->{'discriminator-field'};
372
            $mapping['discriminatorField'] = (string) $attr['name'];
373
        }
374 4
        if (isset($reference->{'discriminator-map'})) {
375
            foreach ($reference->{'discriminator-map'}->{'discriminator-mapping'} as $discriminatorMapping) {
376
                $attr = $discriminatorMapping->attributes();
377
                $mapping['discriminatorMap'][(string) $attr['value']] = (string) $attr['class'];
378
            }
379
        }
380 4
        if (isset($reference->{'default-discriminator-value'})) {
381
            $mapping['defaultDiscriminatorValue'] = (string) $reference->{'default-discriminator-value'}['value'];
382
        }
383 4
        if (isset($reference->{'sort'})) {
384
            foreach ($reference->{'sort'}->{'sort'} as $sort) {
385
                $attr = $sort->attributes();
386
                $mapping['sort'][(string) $attr['field']] = (string) ($attr['order'] ?? 'asc');
387
            }
388
        }
389 4
        if (isset($reference->{'criteria'})) {
390
            foreach ($reference->{'criteria'}->{'criteria'} as $criteria) {
391
                $attr = $criteria->attributes();
392
                $mapping['criteria'][(string) $attr['field']] = (string) $attr['value'];
393
            }
394
        }
395 4
        if (isset($attributes['not-saved'])) {
396
            $mapping['notSaved'] = ((string) $attributes['not-saved'] === 'true');
397
        }
398 4
        if (isset($attributes['also-load'])) {
399
            $mapping['alsoLoadFields'] = explode(',', $attributes['also-load']);
400
        }
401 4
        if (isset($reference->{'prime'})) {
402 1
            foreach ($reference->{'prime'}->{'field'} as $field) {
403 1
                $attr = $field->attributes();
404 1
                $mapping['prime'][] = (string) $attr['name'];
405
            }
406
        }
407
408 4
        $this->addFieldMapping($class, $mapping);
409 4
    }
410
411 1
    private function addIndex(ClassMetadata $class, \SimpleXmlElement $xmlIndex): void
412
    {
413 1
        $attributes = $xmlIndex->attributes();
414
415 1
        $keys = [];
416
417 1
        foreach ($xmlIndex->{'key'} as $key) {
418 1
            $keys[(string) $key['name']] = (string) ($key['order'] ?? 'asc');
419
        }
420
421 1
        $options = [];
422
423 1
        if (isset($attributes['background'])) {
424
            $options['background'] = ((string) $attributes['background'] === 'true');
425
        }
426 1
        if (isset($attributes['drop-dups'])) {
427
            $options['dropDups'] = ((string) $attributes['drop-dups'] === 'true');
428
        }
429 1
        if (isset($attributes['name'])) {
430
            $options['name'] = (string) $attributes['name'];
431
        }
432 1
        if (isset($attributes['sparse'])) {
433
            $options['sparse'] = ((string) $attributes['sparse'] === 'true');
434
        }
435 1
        if (isset($attributes['unique'])) {
436
            $options['unique'] = ((string) $attributes['unique'] === 'true');
437
        }
438
439 1
        if (isset($xmlIndex->{'option'})) {
440
            foreach ($xmlIndex->{'option'} as $option) {
441
                $value = (string) $option['value'];
442
                if ($value === 'true') {
443
                    $value = true;
444
                } elseif ($value === 'false') {
445
                    $value = false;
446
                } elseif (is_numeric($value)) {
447
                    $value = preg_match('/^[-]?\d+$/', $value) ? (int) $value : (float) $value;
448
                }
449
                $options[(string) $option['name']] = $value;
450
            }
451
        }
452
453 1
        if (isset($xmlIndex->{'partial-filter-expression'})) {
454 1
            $partialFilterExpressionMapping = $xmlIndex->{'partial-filter-expression'};
455
456 1
            if (isset($partialFilterExpressionMapping->and)) {
457 1
                foreach ($partialFilterExpressionMapping->and as $and) {
458 1
                    if (! isset($and->field)) {
459
                        continue;
460
                    }
461
462 1
                    $partialFilterExpression = $this->getPartialFilterExpression($and->field);
463 1
                    if (! $partialFilterExpression) {
464
                        continue;
465
                    }
466
467 1
                    $options['partialFilterExpression']['$and'][] = $partialFilterExpression;
468
                }
469 1
            } elseif (isset($partialFilterExpressionMapping->field)) {
470 1
                $partialFilterExpression = $this->getPartialFilterExpression($partialFilterExpressionMapping->field);
471
472 1
                if ($partialFilterExpression) {
473 1
                    $options['partialFilterExpression'] = $partialFilterExpression;
474
                }
475
            }
476
        }
477
478 1
        $class->addIndex($keys, $options);
479 1
    }
480
481 1
    private function getPartialFilterExpression(\SimpleXMLElement $fields): array
482
    {
483 1
        $partialFilterExpression = [];
484 1
        foreach ($fields as $field) {
485 1
            $operator = (string) $field['operator'] ?: null;
486
487 1
            if (! isset($field['value'])) {
488 1
                if (! isset($field->field)) {
489
                    continue;
490
                }
491
492 1
                $nestedExpression = $this->getPartialFilterExpression($field->field);
493 1
                if (! $nestedExpression) {
494
                    continue;
495
                }
496
497 1
                $value = $nestedExpression;
498
            } else {
499 1
                $value = trim((string) $field['value']);
500
            }
501
502 1
            if ($value === 'true') {
503
                $value = true;
504 1
            } elseif ($value === 'false') {
505
                $value = false;
506 1
            } elseif (is_numeric($value)) {
507 1
                $value = preg_match('/^[-]?\d+$/', $value) ? (int) $value : (float) $value;
508
            }
509
510 1
            $partialFilterExpression[(string) $field['name']] = $operator ? ['$' . $operator => $value] : $value;
511
        }
512
513 1
        return $partialFilterExpression;
514
    }
515
516 1
    private function setShardKey(ClassMetadata $class, \SimpleXmlElement $xmlShardkey): void
517
    {
518 1
        $attributes = $xmlShardkey->attributes();
519
520 1
        $keys = [];
521 1
        $options = [];
522 1
        foreach ($xmlShardkey->{'key'} as $key) {
523 1
            $keys[(string) $key['name']] = (string) ($key['order'] ?? 'asc');
524
        }
525
526 1
        if (isset($attributes['unique'])) {
527 1
            $options['unique'] = ((string) $attributes['unique'] === 'true');
528
        }
529
530 1
        if (isset($attributes['numInitialChunks'])) {
531 1
            $options['numInitialChunks'] = (int) $attributes['numInitialChunks'];
532
        }
533
534 1
        if (isset($xmlShardkey->{'option'})) {
535
            foreach ($xmlShardkey->{'option'} as $option) {
536
                $value = (string) $option['value'];
537
                if ($value === 'true') {
538
                    $value = true;
539
                } elseif ($value === 'false') {
540
                    $value = false;
541
                } elseif (is_numeric($value)) {
542
                    $value = preg_match('/^[-]?\d+$/', $value) ? (int) $value : (float) $value;
543
                }
544
                $options[(string) $option['name']] = $value;
545
            }
546
        }
547
548 1
        $class->setShardKey($keys, $options);
549 1
    }
550
551
    /**
552
     * Parses <read-preference> to a format suitable for the underlying driver.
553
     *
554
     * list($readPreference, $tags) = $this->transformReadPreference($xml->{read-preference});
555
     */
556
    private function transformReadPreference(\SimpleXMLElement $xmlReadPreference): array
557
    {
558
        $tags = null;
559
        if (isset($xmlReadPreference->{'tag-set'})) {
560
            $tags = [];
561
            foreach ($xmlReadPreference->{'tag-set'} as $tagSet) {
562
                $set = [];
563
                foreach ($tagSet->tag as $tag) {
564
                    $set[(string) $tag['name']] = (string) $tag['value'];
565
                }
566
                $tags[] = $set;
567
            }
568
        }
569
        return [(string) $xmlReadPreference['mode'], $tags];
570
    }
571
572
    /**
573
     * {@inheritDoc}
574
     */
575 9
    protected function loadMappingFile($file): array
576
    {
577 9
        $result = [];
578
579 9
        $this->validateSchema($file);
580
581 7
        $xmlElement = simplexml_load_file($file);
582
583 7
        foreach (['document', 'embedded-document', 'mapped-superclass', 'query-result-document', 'gridfs-file'] as $type) {
584 7
            if (! isset($xmlElement->$type)) {
585 7
                continue;
586
            }
587
588 7
            foreach ($xmlElement->$type as $documentElement) {
589 7
                $documentName = (string) $documentElement['name'];
590 7
                $result[$documentName] = $documentElement;
591
            }
592
        }
593
594 7
        return $result;
595
    }
596
597 9
    private function validateSchema(string $filename): void
598
    {
599 9
        $document = new DOMDocument();
600 9
        $document->load($filename);
601
602 9
        $previousUseErrors = libxml_use_internal_errors(true);
603
604
        try {
605 9
            libxml_clear_errors();
606
607 9
            if (! $document->schemaValidate(__DIR__ . '/../../../../../../doctrine-mongo-mapping.xsd')) {
608 2
                throw MappingException::xmlMappingFileInvalid($filename, $this->formatErrors(libxml_get_errors()));
609
            }
610 7
        } finally {
611 9
            libxml_use_internal_errors($previousUseErrors);
612
        }
613 7
    }
614
615
    /**
616
     * @param LibXMLError[] $xmlErrors
617
     */
618 2
    private function formatErrors(array $xmlErrors): string
619
    {
620
        return implode("\n", array_map(function (LibXMLError $error): string {
621 2
            return sprintf('Line %d:%d: %s', $error->line, $error->column, $error->message);
622 2
        }, $xmlErrors));
623
    }
624
625 7
    private function addGridFSMappings(ClassMetadata $class, \SimpleXMLElement $xmlRoot): void
626
    {
627 7
        if (! $class->isFile) {
628 6
            return;
629
        }
630
631 1
        foreach (self::DEFAULT_GRIDFS_MAPPINGS as $name => $mapping) {
632 1
            if (! isset($xmlRoot->{$name})) {
633
                continue;
634
            }
635
636 1
            if (isset($xmlRoot->{$name}->attributes()['fieldName'])) {
637 1
                $mapping['fieldName'] = (string) $xmlRoot->{$name}->attributes()['fieldName'];
638
            }
639
640 1
            $this->addFieldMapping($class, $mapping);
641
        }
642
643 1
        if (! isset($xmlRoot->metadata)) {
644
            return;
645
        }
646
647 1
        $xmlRoot->metadata->addAttribute('field', 'metadata');
648 1
        $this->addEmbedMapping($class, $xmlRoot->metadata, 'one');
649 1
    }
650
}
651