Completed
Push — master ( 26ecbc...8c0c5d )
by Maciej
14s
created

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php (2 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;
6
7
use BadMethodCallException;
8
use Doctrine\Common\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
9
use Doctrine\Instantiator\Instantiator;
10
use Doctrine\Instantiator\InstantiatorInterface;
11
use Doctrine\ODM\MongoDB\Id\AbstractIdGenerator;
12
use Doctrine\ODM\MongoDB\LockException;
13
use Doctrine\ODM\MongoDB\Types\Type;
14
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
15
use InvalidArgumentException;
16
use LogicException;
17
use ProxyManager\Proxy\GhostObjectInterface;
18
use ReflectionClass;
19
use ReflectionProperty;
20
use function array_filter;
21
use function array_key_exists;
22
use function array_keys;
23
use function array_map;
24
use function array_pop;
25
use function class_exists;
26
use function constant;
27
use function count;
28
use function get_class;
29
use function in_array;
30
use function is_array;
31
use function is_string;
32
use function is_subclass_of;
33
use function ltrim;
34
use function sprintf;
35
use function strtolower;
36
use function strtoupper;
37
38
/**
39
 * A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata
40
 * of a document and it's references.
41
 *
42
 * Once populated, ClassMetadata instances are usually cached in a serialized form.
43
 *
44
 * <b>IMPORTANT NOTE:</b>
45
 *
46
 * The fields of this class are only public for 2 reasons:
47
 * 1) To allow fast READ access.
48
 * 2) To drastically reduce the size of a serialized instance (private/protected members
49
 *    get the whole class name, namespace inclusive, prepended to every property in
50
 *    the serialized representation).
51
 */
52
class ClassMetadata implements BaseClassMetadata
53
{
54
    /* The Id generator types. */
55
    /**
56
     * AUTO means Doctrine will automatically create a new \MongoDB\BSON\ObjectId instance for us.
57
     */
58
    public const GENERATOR_TYPE_AUTO = 1;
59
60
    /**
61
     * INCREMENT means a separate collection is used for maintaining and incrementing id generation.
62
     * Offers full portability.
63
     */
64
    public const GENERATOR_TYPE_INCREMENT = 2;
65
66
    /**
67
     * UUID means Doctrine will generate a uuid for us.
68
     */
69
    public const GENERATOR_TYPE_UUID = 3;
70
71
    /**
72
     * ALNUM means Doctrine will generate Alpha-numeric string identifiers, using the INCREMENT
73
     * generator to ensure identifier uniqueness
74
     */
75
    public const GENERATOR_TYPE_ALNUM = 4;
76
77
    /**
78
     * CUSTOM means Doctrine expect a class parameter. It will then try to initiate that class
79
     * and pass other options to the generator. It will throw an Exception if the class
80
     * does not exist or if an option was passed for that there is not setter in the new
81
     * generator class.
82
     *
83
     * The class  will have to be a subtype of AbstractIdGenerator.
84
     */
85
    public const GENERATOR_TYPE_CUSTOM = 5;
86
87
    /**
88
     * NONE means Doctrine will not generate any id for us and you are responsible for manually
89
     * assigning an id.
90
     */
91
    public const GENERATOR_TYPE_NONE = 6;
92
93
    /**
94
     * Default discriminator field name.
95
     *
96
     * This is used for associations value for associations where a that do not define a "targetDocument" or
97
     * "discriminatorField" option in their mapping.
98
     */
99
    public const DEFAULT_DISCRIMINATOR_FIELD = '_doctrine_class_name';
100
101
    public const REFERENCE_ONE  = 1;
102
    public const REFERENCE_MANY = 2;
103
    public const EMBED_ONE      = 3;
104
    public const EMBED_MANY     = 4;
105
    public const MANY           = 'many';
106
    public const ONE            = 'one';
107
108
    /**
109
     * The types of storeAs references
110
     */
111
    public const REFERENCE_STORE_AS_ID             = 'id';
112
    public const REFERENCE_STORE_AS_DB_REF         = 'dbRef';
113
    public const REFERENCE_STORE_AS_DB_REF_WITH_DB = 'dbRefWithDb';
114
    public const REFERENCE_STORE_AS_REF            = 'ref';
115
116
    /* The inheritance mapping types */
117
    /**
118
     * NONE means the class does not participate in an inheritance hierarchy
119
     * and therefore does not need an inheritance mapping type.
120
     */
121
    public const INHERITANCE_TYPE_NONE = 1;
122
123
    /**
124
     * SINGLE_COLLECTION means the class will be persisted according to the rules of
125
     * <tt>Single Collection Inheritance</tt>.
126
     */
127
    public const INHERITANCE_TYPE_SINGLE_COLLECTION = 2;
128
129
    /**
130
     * COLLECTION_PER_CLASS means the class will be persisted according to the rules
131
     * of <tt>Concrete Collection Inheritance</tt>.
132
     */
133
    public const INHERITANCE_TYPE_COLLECTION_PER_CLASS = 3;
134
135
    /**
136
     * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
137
     * by doing a property-by-property comparison with the original data. This will
138
     * be done for all entities that are in MANAGED state at commit-time.
139
     *
140
     * This is the default change tracking policy.
141
     */
142
    public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
143
144
    /**
145
     * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
146
     * by doing a property-by-property comparison with the original data. This will
147
     * be done only for entities that were explicitly saved (through persist() or a cascade).
148
     */
149
    public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
150
151
    /**
152
     * NOTIFY means that Doctrine relies on the entities sending out notifications
153
     * when their properties change. Such entity classes must implement
154
     * the <tt>NotifyPropertyChanged</tt> interface.
155
     */
156
    public const CHANGETRACKING_NOTIFY = 3;
157
158
    /**
159
     * SET means that fields will be written to the database using a $set operator
160
     */
161
    public const STORAGE_STRATEGY_SET = 'set';
162
163
    /**
164
     * INCREMENT means that fields will be written to the database by calculating
165
     * the difference and using the $inc operator
166
     */
167
    public const STORAGE_STRATEGY_INCREMENT = 'increment';
168
169
    public const STORAGE_STRATEGY_PUSH_ALL         = 'pushAll';
170
    public const STORAGE_STRATEGY_ADD_TO_SET       = 'addToSet';
171
    public const STORAGE_STRATEGY_ATOMIC_SET       = 'atomicSet';
172
    public const STORAGE_STRATEGY_ATOMIC_SET_ARRAY = 'atomicSetArray';
173
    public const STORAGE_STRATEGY_SET_ARRAY        = 'setArray';
174
175
    private const ALLOWED_GRIDFS_FIELDS = ['_id', 'chunkSize', 'filename', 'length', 'metadata', 'uploadDate'];
176
177
    /**
178
     * READ-ONLY: The name of the mongo database the document is mapped to.
179
     *
180
     * @var string|null
181
     */
182
    public $db;
183
184
    /**
185
     * READ-ONLY: The name of the mongo collection the document is mapped to.
186
     *
187
     * @var string
188
     */
189
    public $collection;
190
191
    /**
192
     * READ-ONLY: The name of the GridFS bucket the document is mapped to.
193
     *
194
     * @var string
195
     */
196
    public $bucketName = 'fs';
197
198
    /**
199
     * READ-ONLY: If the collection should be a fixed size.
200
     *
201
     * @var bool
202
     */
203
    public $collectionCapped = false;
204
205
    /**
206
     * READ-ONLY: If the collection is fixed size, its size in bytes.
207
     *
208
     * @var int|null
209
     */
210
    public $collectionSize;
211
212
    /**
213
     * READ-ONLY: If the collection is fixed size, the maximum number of elements to store in the collection.
214
     *
215
     * @var int|null
216
     */
217
    public $collectionMax;
218
219
    /**
220
     * READ-ONLY Describes how MongoDB clients route read operations to the members of a replica set.
221
     *
222
     * @var string|null
223
     */
224
    public $readPreference;
225
226
    /**
227
     * READ-ONLY Associated with readPreference Allows to specify criteria so that your application can target read
228
     * operations to specific members, based on custom parameters.
229
     *
230
     * @var string[][]
231
     */
232
    public $readPreferenceTags = [];
233
234
    /**
235
     * READ-ONLY: Describes the level of acknowledgement requested from MongoDB for write operations.
236
     *
237
     * @var string|int|null
238
     */
239
    public $writeConcern;
240
241
    /**
242
     * READ-ONLY: The field name of the document identifier.
243
     *
244
     * @var string|null
245
     */
246
    public $identifier;
247
248
    /**
249
     * READ-ONLY: The array of indexes for the document collection.
250
     *
251
     * @var array
252
     */
253
    public $indexes = [];
254
255
    /**
256
     * READ-ONLY: Keys and options describing shard key. Only for sharded collections.
257
     *
258
     * @var array<string, array>
259
     */
260
    public $shardKey = [];
261
262
    /**
263
     * READ-ONLY: The name of the document class.
264
     *
265
     * @var string
266
     */
267
    public $name;
268
269
    /**
270
     * READ-ONLY: The name of the document class that is at the root of the mapped document inheritance
271
     * hierarchy. If the document is not part of a mapped inheritance hierarchy this is the same
272
     * as {@link $documentName}.
273
     *
274
     * @var string
275
     */
276
    public $rootDocumentName;
277
278
    /**
279
     * The name of the custom repository class used for the document class.
280
     * (Optional).
281
     *
282
     * @var string|null
283
     */
284
    public $customRepositoryClassName;
285
286
    /**
287
     * READ-ONLY: The names of the parent classes (ancestors).
288
     *
289
     * @var array
290
     */
291
    public $parentClasses = [];
292
293
    /**
294
     * READ-ONLY: The names of all subclasses (descendants).
295
     *
296
     * @var array
297
     */
298
    public $subClasses = [];
299
300
    /**
301
     * The ReflectionProperty instances of the mapped class.
302
     *
303
     * @var ReflectionProperty[]
304
     */
305
    public $reflFields = [];
306
307
    /**
308
     * READ-ONLY: The inheritance mapping type used by the class.
309
     *
310
     * @var int
311
     */
312
    public $inheritanceType = self::INHERITANCE_TYPE_NONE;
313
314
    /**
315
     * READ-ONLY: The Id generator type used by the class.
316
     *
317
     * @var int
318
     */
319
    public $generatorType = self::GENERATOR_TYPE_AUTO;
320
321
    /**
322
     * READ-ONLY: The Id generator options.
323
     *
324
     * @var array
325
     */
326
    public $generatorOptions = [];
327
328
    /**
329
     * READ-ONLY: The ID generator used for generating IDs for this class.
330
     *
331
     * @var AbstractIdGenerator|null
332
     */
333
    public $idGenerator;
334
335
    /**
336
     * READ-ONLY: The field mappings of the class.
337
     * Keys are field names and values are mapping definitions.
338
     *
339
     * The mapping definition array has the following values:
340
     *
341
     * - <b>fieldName</b> (string)
342
     * The name of the field in the Document.
343
     *
344
     * - <b>id</b> (boolean, optional)
345
     * Marks the field as the primary key of the document. Multiple fields of an
346
     * document can have the id attribute, forming a composite key.
347
     *
348
     * @var array
349
     */
350
    public $fieldMappings = [];
351
352
    /**
353
     * READ-ONLY: The association mappings of the class.
354
     * Keys are field names and values are mapping definitions.
355
     *
356
     * @var array
357
     */
358
    public $associationMappings = [];
359
360
    /**
361
     * READ-ONLY: Array of fields to also load with a given method.
362
     *
363
     * @var array
364
     */
365
    public $alsoLoadMethods = [];
366
367
    /**
368
     * READ-ONLY: The registered lifecycle callbacks for documents of this class.
369
     *
370
     * @var array
371
     */
372
    public $lifecycleCallbacks = [];
373
374
    /**
375
     * READ-ONLY: The discriminator value of this class.
376
     *
377
     * <b>This does only apply to the JOINED and SINGLE_COLLECTION inheritance mapping strategies
378
     * where a discriminator field is used.</b>
379
     *
380
     * @see discriminatorField
381
     *
382
     * @var mixed
383
     */
384
    public $discriminatorValue;
385
386
    /**
387
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
388
     *
389
     * <b>This does only apply to the SINGLE_COLLECTION inheritance mapping strategy
390
     * where a discriminator field is used.</b>
391
     *
392
     * @see discriminatorField
393
     *
394
     * @var mixed
395
     */
396
    public $discriminatorMap = [];
397
398
    /**
399
     * READ-ONLY: The definition of the discriminator field used in SINGLE_COLLECTION
400
     * inheritance mapping.
401
     *
402
     * @var string|null
403
     */
404
    public $discriminatorField;
405
406
    /**
407
     * READ-ONLY: The default value for discriminatorField in case it's not set in the document
408
     *
409
     * @see discriminatorField
410
     *
411
     * @var string|null
412
     */
413
    public $defaultDiscriminatorValue;
414
415
    /**
416
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
417
     *
418
     * @var bool
419
     */
420
    public $isMappedSuperclass = false;
421
422
    /**
423
     * READ-ONLY: Whether this class describes the mapping of a embedded document.
424
     *
425
     * @var bool
426
     */
427
    public $isEmbeddedDocument = false;
428
429
    /**
430
     * READ-ONLY: Whether this class describes the mapping of an aggregation result document.
431
     *
432
     * @var bool
433
     */
434
    public $isQueryResultDocument = false;
435
436
    /**
437
     * READ-ONLY: Whether this class describes the mapping of a gridFS file
438
     *
439
     * @var bool
440
     */
441
    public $isFile = false;
442
443
    /**
444
     * READ-ONLY: The default chunk size in bytes for the file
445
     *
446
     * @var int|null
447
     */
448
    public $chunkSizeBytes;
449
450
    /**
451
     * READ-ONLY: The policy used for change-tracking on entities of this class.
452
     *
453
     * @var int
454
     */
455
    public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
456
457
    /**
458
     * READ-ONLY: A flag for whether or not instances of this class are to be versioned
459
     * with optimistic locking.
460
     *
461
     * @var bool $isVersioned
462
     */
463
    public $isVersioned = false;
464
465
    /**
466
     * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
467
     *
468
     * @var string|null $versionField
469
     */
470
    public $versionField;
471
472
    /**
473
     * READ-ONLY: A flag for whether or not instances of this class are to allow pessimistic
474
     * locking.
475
     *
476
     * @var bool $isLockable
477
     */
478
    public $isLockable = false;
479
480
    /**
481
     * READ-ONLY: The name of the field which is used for locking a document.
482
     *
483
     * @var mixed $lockField
484
     */
485
    public $lockField;
486
487
    /**
488
     * The ReflectionClass instance of the mapped class.
489
     *
490
     * @var ReflectionClass
491
     */
492
    public $reflClass;
493
494
    /**
495
     * READ_ONLY: A flag for whether or not this document is read-only.
496
     *
497
     * @var bool
498
     */
499
    public $isReadOnly;
500
501
    /** @var InstantiatorInterface */
502
    private $instantiator;
503
504
    /**
505
     * Initializes a new ClassMetadata instance that will hold the object-document mapping
506
     * metadata of the class with the given name.
507
     */
508 1564
    public function __construct(string $documentName)
509
    {
510 1564
        $this->name             = $documentName;
511 1564
        $this->rootDocumentName = $documentName;
512 1564
        $this->reflClass        = new ReflectionClass($documentName);
513 1564
        $this->setCollection($this->reflClass->getShortName());
514 1564
        $this->instantiator = new Instantiator();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Doctrine\Instantiator\Instantiator() of type object<Doctrine\Instantiator\Instantiator> is incompatible with the declared type object<Doctrine\Instanti...\InstantiatorInterface> of property $instantiator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
515 1564
    }
516
517
    /**
518
     * Helper method to get reference id of ref* type references
519
     *
520
     * @internal
521
     *
522
     * @param mixed $reference
523
     *
524
     * @return mixed
525
     */
526 116
    public static function getReferenceId($reference, string $storeAs)
527
    {
528 116
        return $storeAs === self::REFERENCE_STORE_AS_ID ? $reference : $reference[self::getReferencePrefix($storeAs) . 'id'];
529
    }
530
531
    /**
532
     * Returns the reference prefix used for a reference
533
     */
534 184
    private static function getReferencePrefix(string $storeAs) : string
535
    {
536 184
        if (! in_array($storeAs, [self::REFERENCE_STORE_AS_REF, self::REFERENCE_STORE_AS_DB_REF, self::REFERENCE_STORE_AS_DB_REF_WITH_DB])) {
537
            throw new LogicException('Can only get a reference prefix for DBRef and reference arrays');
538
        }
539
540 184
        return $storeAs === self::REFERENCE_STORE_AS_REF ? '' : '$';
541
    }
542
543
    /**
544
     * Returns a fully qualified field name for a given reference
545
     *
546
     * @internal
547
     *
548
     * @param string $pathPrefix The field path prefix
549
     */
550 137
    public static function getReferenceFieldName(string $storeAs, string $pathPrefix = '') : string
551
    {
552 137
        if ($storeAs === self::REFERENCE_STORE_AS_ID) {
553 97
            return $pathPrefix;
554
        }
555
556 125
        return ($pathPrefix ? $pathPrefix . '.' : '') . static::getReferencePrefix($storeAs) . 'id';
557
    }
558
559
    /**
560
     * {@inheritDoc}
561
     */
562 1448
    public function getReflectionClass() : ReflectionClass
563
    {
564 1448
        return $this->reflClass;
565
    }
566
567
    /**
568
     * {@inheritDoc}
569
     */
570 323
    public function isIdentifier($fieldName) : bool
571
    {
572 323
        return $this->identifier === $fieldName;
573
    }
574
575
    /**
576
     * INTERNAL:
577
     * Sets the mapped identifier field of this class.
578
     */
579 914
    public function setIdentifier(?string $identifier) : void
580
    {
581 914
        $this->identifier = $identifier;
582 914
    }
583
584
    /**
585
     * {@inheritDoc}
586
     *
587
     * Since MongoDB only allows exactly one identifier field
588
     * this will always return an array with only one value
589
     */
590 11
    public function getIdentifier() : array
591
    {
592 11
        return [$this->identifier];
593
    }
594
595
    /**
596
     * Since MongoDB only allows exactly one identifier field
597
     * this will always return an array with only one value
598
     *
599
     * return (string|null)[]
600
     */
601 99
    public function getIdentifierFieldNames() : array
602
    {
603 99
        return [$this->identifier];
604
    }
605
606
    /**
607
     * {@inheritDoc}
608
     */
609 918
    public function hasField($fieldName) : bool
610
    {
611 918
        return isset($this->fieldMappings[$fieldName]);
612
    }
613
614
    /**
615
     * Sets the inheritance type used by the class and it's subclasses.
616
     */
617 930
    public function setInheritanceType(int $type) : void
618
    {
619 930
        $this->inheritanceType = $type;
620 930
    }
621
622
    /**
623
     * Checks whether a mapped field is inherited from an entity superclass.
624
     */
625 1439
    public function isInheritedField(string $fieldName) : bool
626
    {
627 1439
        return isset($this->fieldMappings[$fieldName]['inherited']);
628
    }
629
630
    /**
631
     * Registers a custom repository class for the document class.
632
     */
633 861
    public function setCustomRepositoryClass(?string $repositoryClassName) : void
634
    {
635 861
        if ($this->isEmbeddedDocument || $this->isQueryResultDocument) {
636
            return;
637
        }
638
639 861
        $this->customRepositoryClassName = $repositoryClassName;
640 861
    }
641
642
    /**
643
     * Dispatches the lifecycle event of the given document by invoking all
644
     * registered callbacks.
645
     *
646
     * @throws InvalidArgumentException If document class is not this class or
647
     *                                   a Proxy of this class.
648
     */
649 645
    public function invokeLifecycleCallbacks(string $event, object $document, ?array $arguments = null) : void
650
    {
651 645
        if (! $document instanceof $this->name) {
652 1
            throw new InvalidArgumentException(sprintf('Expected document class "%s"; found: "%s"', $this->name, get_class($document)));
653
        }
654
655 644
        if (empty($this->lifecycleCallbacks[$event])) {
656 629
            return;
657
        }
658
659 189
        foreach ($this->lifecycleCallbacks[$event] as $callback) {
660 189
            if ($arguments !== null) {
661 188
                $document->$callback(...$arguments);
662
            } else {
663 189
                $document->$callback();
664
            }
665
        }
666 189
    }
667
668
    /**
669
     * Checks whether the class has callbacks registered for a lifecycle event.
670
     */
671
    public function hasLifecycleCallbacks(string $event) : bool
672
    {
673
        return ! empty($this->lifecycleCallbacks[$event]);
674
    }
675
676
    /**
677
     * Gets the registered lifecycle callbacks for an event.
678
     */
679
    public function getLifecycleCallbacks(string $event) : array
680
    {
681
        return $this->lifecycleCallbacks[$event] ?? [];
682
    }
683
684
    /**
685
     * Adds a lifecycle callback for documents of this class.
686
     *
687
     * If the callback is already registered, this is a NOOP.
688
     */
689 832
    public function addLifecycleCallback(string $callback, string $event) : void
690
    {
691 832
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
692 1
            return;
693
        }
694
695 832
        $this->lifecycleCallbacks[$event][] = $callback;
696 832
    }
697
698
    /**
699
     * Sets the lifecycle callbacks for documents of this class.
700
     *
701
     * Any previously registered callbacks are overwritten.
702
     */
703 913
    public function setLifecycleCallbacks(array $callbacks) : void
704
    {
705 913
        $this->lifecycleCallbacks = $callbacks;
706 913
    }
707
708
    /**
709
     * Registers a method for loading document data before field hydration.
710
     *
711
     * Note: A method may be registered multiple times for different fields.
712
     * it will be invoked only once for the first field found.
713
     *
714
     * @param array|string $fields Database field name(s)
715
     */
716 14
    public function registerAlsoLoadMethod(string $method, $fields) : void
717
    {
718 14
        $this->alsoLoadMethods[$method] = is_array($fields) ? $fields : [$fields];
719 14
    }
720
721
    /**
722
     * Sets the AlsoLoad methods for documents of this class.
723
     *
724
     * Any previously registered methods are overwritten.
725
     */
726 913
    public function setAlsoLoadMethods(array $methods) : void
727
    {
728 913
        $this->alsoLoadMethods = $methods;
729 913
    }
730
731
    /**
732
     * Sets the discriminator field.
733
     *
734
     * The field name is the the unmapped database field. Discriminator values
735
     * are only used to discern the hydration class and are not mapped to class
736
     * properties.
737
     *
738
     * @param array|string|null $discriminatorField
739
     *
740
     * @throws MappingException If the discriminator field conflicts with the
741
     *                          "name" attribute of a mapped field.
742
     */
743 939
    public function setDiscriminatorField($discriminatorField) : void
744
    {
745 939
        if ($this->isFile) {
746
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
747
        }
748
749 939
        if ($discriminatorField === null) {
750 870
            $this->discriminatorField = null;
751
752 870
            return;
753
        }
754
755
        // @todo: deprecate, document and remove this:
756
        // Handle array argument with name/fieldName keys for BC
757 138
        if (is_array($discriminatorField)) {
758
            if (isset($discriminatorField['name'])) {
759
                $discriminatorField = $discriminatorField['name'];
760
            } elseif (isset($discriminatorField['fieldName'])) {
761
                $discriminatorField = $discriminatorField['fieldName'];
762
            }
763
        }
764
765 138
        foreach ($this->fieldMappings as $fieldMapping) {
766 4
            if ($discriminatorField === $fieldMapping['name']) {
767 4
                throw MappingException::discriminatorFieldConflict($this->name, $discriminatorField);
768
            }
769
        }
770
771 137
        $this->discriminatorField = $discriminatorField;
772 137
    }
773
774
    /**
775
     * Sets the discriminator values used by this class.
776
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
777
     *
778
     * @throws MappingException
779
     */
780 932
    public function setDiscriminatorMap(array $map) : void
781
    {
782 932
        if ($this->isFile) {
783
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
784
        }
785
786 932
        foreach ($map as $value => $className) {
787 132
            $this->discriminatorMap[$value] = $className;
788 132
            if ($this->name === $className) {
789 124
                $this->discriminatorValue = $value;
790
            } else {
791 131
                if (! class_exists($className)) {
792
                    throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
793
                }
794 131
                if (is_subclass_of($className, $this->name)) {
795 132
                    $this->subClasses[] = $className;
796
                }
797
            }
798
        }
799 932
    }
800
801
    /**
802
     * Sets the default discriminator value to be used for this class
803
     * Used for SINGLE_TABLE inheritance mapping strategies if the document has no discriminator value
804
     *
805
     * @throws MappingException
806
     */
807 916
    public function setDefaultDiscriminatorValue(?string $defaultDiscriminatorValue) : void
808
    {
809 916
        if ($this->isFile) {
810
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
811
        }
812
813 916
        if ($defaultDiscriminatorValue === null) {
814 913
            $this->defaultDiscriminatorValue = null;
815
816 913
            return;
817
        }
818
819 68
        if (! array_key_exists($defaultDiscriminatorValue, $this->discriminatorMap)) {
820
            throw MappingException::invalidDiscriminatorValue($defaultDiscriminatorValue, $this->name);
821
        }
822
823 68
        $this->defaultDiscriminatorValue = $defaultDiscriminatorValue;
824 68
    }
825
826
    /**
827
     * Sets the discriminator value for this class.
828
     * Used for JOINED/SINGLE_TABLE inheritance and multiple document types in a single
829
     * collection.
830
     *
831
     * @throws MappingException
832
     */
833 3
    public function setDiscriminatorValue(string $value) : void
834
    {
835 3
        if ($this->isFile) {
836
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
837
        }
838
839 3
        $this->discriminatorMap[$value] = $this->name;
840 3
        $this->discriminatorValue       = $value;
841 3
    }
842
843
    /**
844
     * Add a index for this Document.
845
     */
846 216
    public function addIndex(array $keys, array $options = []) : void
847
    {
848 216
        $this->indexes[] = [
849
            'keys' => array_map(static function ($value) {
850 216
                if ($value === 1 || $value === -1) {
851 65
                    return $value;
852
                }
853 216
                if (is_string($value)) {
854 216
                    $lower = strtolower($value);
855 216
                    if ($lower === 'asc') {
856 209
                        return 1;
857
                    }
858
859 72
                    if ($lower === 'desc') {
860
                        return -1;
861
                    }
862
                }
863 72
                return $value;
864 216
            }, $keys),
865 216
            'options' => $options,
866
        ];
867 216
    }
868
869
    /**
870
     * Returns the array of indexes for this Document.
871
     */
872 26
    public function getIndexes() : array
873
    {
874 26
        return $this->indexes;
875
    }
876
877
    /**
878
     * Checks whether this document has indexes or not.
879
     */
880
    public function hasIndexes() : bool
881
    {
882
        return $this->indexes ? true : false;
883
    }
884
885
    /**
886
     * Set shard key for this Document.
887
     *
888
     * @throws MappingException
889
     */
890 100
    public function setShardKey(array $keys, array $options = []) : void
891
    {
892 100
        if ($this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION && $this->shardKey !== []) {
893 2
            throw MappingException::shardKeyInSingleCollInheritanceSubclass($this->getName());
894
        }
895
896 100
        if ($this->isEmbeddedDocument) {
897 2
            throw MappingException::embeddedDocumentCantHaveShardKey($this->getName());
898
        }
899
900 98
        foreach (array_keys($keys) as $field) {
901 98
            if (! isset($this->fieldMappings[$field])) {
902 91
                continue;
903
            }
904
905 7
            if (in_array($this->fieldMappings[$field]['type'], ['many', 'collection'])) {
906 3
                throw MappingException::noMultiKeyShardKeys($this->getName(), $field);
907
            }
908
909 4
            if ($this->fieldMappings[$field]['strategy'] !== static::STORAGE_STRATEGY_SET) {
910 4
                throw MappingException::onlySetStrategyAllowedInShardKey($this->getName(), $field);
911
            }
912
        }
913
914 94
        $this->shardKey = [
915
            'keys' => array_map(static function ($value) {
916 94
                if ($value === 1 || $value === -1) {
917 5
                    return $value;
918
                }
919 94
                if (is_string($value)) {
920 94
                    $lower = strtolower($value);
921 94
                    if ($lower === 'asc') {
922 92
                        return 1;
923
                    }
924
925 67
                    if ($lower === 'desc') {
926
                        return -1;
927
                    }
928
                }
929 67
                return $value;
930 94
            }, $keys),
931 94
            'options' => $options,
932
        ];
933 94
    }
934
935 26
    public function getShardKey() : array
936
    {
937 26
        return $this->shardKey;
938
    }
939
940
    /**
941
     * Checks whether this document has shard key or not.
942
     */
943 1159
    public function isSharded() : bool
944
    {
945 1159
        return $this->shardKey ? true : false;
946
    }
947
948
    /**
949
     * Sets the read preference used by this class.
950
     */
951 913
    public function setReadPreference(?string $readPreference, array $tags) : void
952
    {
953 913
        $this->readPreference     = $readPreference;
954 913
        $this->readPreferenceTags = $tags;
955 913
    }
956
957
    /**
958
     * Sets the write concern used by this class.
959
     *
960
     * @param string|int|null $writeConcern
961
     */
962 923
    public function setWriteConcern($writeConcern) : void
963
    {
964 923
        $this->writeConcern = $writeConcern;
965 923
    }
966
967
    /**
968
     * @return int|string|null
969
     */
970 11
    public function getWriteConcern()
971
    {
972 11
        return $this->writeConcern;
973
    }
974
975
    /**
976
     * Whether there is a write concern configured for this class.
977
     */
978 592
    public function hasWriteConcern() : bool
979
    {
980 592
        return $this->writeConcern !== null;
981
    }
982
983
    /**
984
     * Sets the change tracking policy used by this class.
985
     */
986 915
    public function setChangeTrackingPolicy(int $policy) : void
987
    {
988 915
        $this->changeTrackingPolicy = $policy;
989 915
    }
990
991
    /**
992
     * Whether the change tracking policy of this class is "deferred explicit".
993
     */
994 68
    public function isChangeTrackingDeferredExplicit() : bool
995
    {
996 68
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
997
    }
998
999
    /**
1000
     * Whether the change tracking policy of this class is "deferred implicit".
1001
     */
1002 615
    public function isChangeTrackingDeferredImplicit() : bool
1003
    {
1004 615
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
1005
    }
1006
1007
    /**
1008
     * Whether the change tracking policy of this class is "notify".
1009
     */
1010 342
    public function isChangeTrackingNotify() : bool
1011
    {
1012 342
        return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
1013
    }
1014
1015
    /**
1016
     * Gets the ReflectionProperties of the mapped class.
1017
     */
1018 1
    public function getReflectionProperties() : array
1019
    {
1020 1
        return $this->reflFields;
1021
    }
1022
1023
    /**
1024
     * Gets a ReflectionProperty for a specific field of the mapped class.
1025
     */
1026 98
    public function getReflectionProperty(string $name) : ReflectionProperty
1027
    {
1028 98
        return $this->reflFields[$name];
1029
    }
1030
1031
    /**
1032
     * {@inheritDoc}
1033
     */
1034 1447
    public function getName() : string
1035
    {
1036 1447
        return $this->name;
1037
    }
1038
1039
    /**
1040
     * Returns the database this Document is mapped to.
1041
     */
1042 1366
    public function getDatabase() : ?string
1043
    {
1044 1366
        return $this->db;
1045
    }
1046
1047
    /**
1048
     * Set the database this Document is mapped to.
1049
     */
1050 111
    public function setDatabase(?string $db) : void
1051
    {
1052 111
        $this->db = $db;
1053 111
    }
1054
1055
    /**
1056
     * Get the collection this Document is mapped to.
1057
     */
1058 1358
    public function getCollection() : string
1059
    {
1060 1358
        return $this->collection;
1061
    }
1062
1063
    /**
1064
     * Sets the collection this Document is mapped to.
1065
     *
1066
     * @param array|string $name
1067
     *
1068
     * @throws InvalidArgumentException
1069
     */
1070 1564
    public function setCollection($name) : void
1071
    {
1072 1564
        if (is_array($name)) {
1073 1
            if (! isset($name['name'])) {
1074
                throw new InvalidArgumentException('A name key is required when passing an array to setCollection()');
1075
            }
1076 1
            $this->collectionCapped = $name['capped'] ?? false;
1077 1
            $this->collectionSize   = $name['size'] ?? 0;
1078 1
            $this->collectionMax    = $name['max'] ?? 0;
1079 1
            $this->collection       = $name['name'];
1080
        } else {
1081 1564
            $this->collection = $name;
1082
        }
1083 1564
    }
1084
1085 19
    public function getBucketName() : ?string
1086
    {
1087 19
        return $this->bucketName;
1088
    }
1089
1090 1
    public function setBucketName(string $bucketName) : void
1091
    {
1092 1
        $this->bucketName = $bucketName;
1093 1
        $this->setCollection($bucketName . '.files');
1094 1
    }
1095
1096 10
    public function getChunkSizeBytes() : ?int
1097
    {
1098 10
        return $this->chunkSizeBytes;
1099
    }
1100
1101 79
    public function setChunkSizeBytes(int $chunkSizeBytes) : void
1102
    {
1103 79
        $this->chunkSizeBytes = $chunkSizeBytes;
1104 79
    }
1105
1106
    /**
1107
     * Get whether or not the documents collection is capped.
1108
     */
1109 5
    public function getCollectionCapped() : bool
1110
    {
1111 5
        return $this->collectionCapped;
1112
    }
1113
1114
    /**
1115
     * Set whether or not the documents collection is capped.
1116
     */
1117 1
    public function setCollectionCapped(bool $bool) : void
1118
    {
1119 1
        $this->collectionCapped = $bool;
1120 1
    }
1121
1122
    /**
1123
     * Get the collection size
1124
     */
1125 5
    public function getCollectionSize() : ?int
1126
    {
1127 5
        return $this->collectionSize;
1128
    }
1129
1130
    /**
1131
     * Set the collection size.
1132
     */
1133 1
    public function setCollectionSize(int $size) : void
1134
    {
1135 1
        $this->collectionSize = $size;
1136 1
    }
1137
1138
    /**
1139
     * Get the collection max.
1140
     */
1141 5
    public function getCollectionMax() : ?int
1142
    {
1143 5
        return $this->collectionMax;
1144
    }
1145
1146
    /**
1147
     * Set the collection max.
1148
     */
1149 1
    public function setCollectionMax(int $max) : void
1150
    {
1151 1
        $this->collectionMax = $max;
1152 1
    }
1153
1154
    /**
1155
     * Returns TRUE if this Document is mapped to a collection FALSE otherwise.
1156
     */
1157
    public function isMappedToCollection() : bool
1158
    {
1159
        return $this->collection ? true : false;
1160
    }
1161
1162
    /**
1163
     * Validates the storage strategy of a mapping for consistency
1164
     *
1165
     * @throws MappingException
1166
     */
1167 1466
    private function applyStorageStrategy(array &$mapping) : void
1168
    {
1169 1466
        if (! isset($mapping['type']) || isset($mapping['id'])) {
1170 1446
            return;
1171
        }
1172
1173
        switch (true) {
1174 1431
            case $mapping['type'] === 'int':
1175 1430
            case $mapping['type'] === 'float':
1176 879
                $defaultStrategy   = self::STORAGE_STRATEGY_SET;
1177 879
                $allowedStrategies = [self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_INCREMENT];
1178 879
                break;
1179
1180 1430
            case $mapping['type'] === 'many':
1181 1126
                $defaultStrategy   = CollectionHelper::DEFAULT_STRATEGY;
1182
                $allowedStrategies = [
1183 1126
                    self::STORAGE_STRATEGY_PUSH_ALL,
1184 1126
                    self::STORAGE_STRATEGY_ADD_TO_SET,
1185 1126
                    self::STORAGE_STRATEGY_SET,
1186 1126
                    self::STORAGE_STRATEGY_SET_ARRAY,
1187 1126
                    self::STORAGE_STRATEGY_ATOMIC_SET,
1188 1126
                    self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY,
1189
                ];
1190 1126
                break;
1191
1192
            default:
1193 1418
                $defaultStrategy   = self::STORAGE_STRATEGY_SET;
1194 1418
                $allowedStrategies = [self::STORAGE_STRATEGY_SET];
1195
        }
1196
1197 1431
        if (! isset($mapping['strategy'])) {
1198 1423
            $mapping['strategy'] = $defaultStrategy;
1199
        }
1200
1201 1431
        if (! in_array($mapping['strategy'], $allowedStrategies)) {
1202
            throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']);
1203
        }
1204
1205 1431
        if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide']
1206 1431
            && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])) {
1207 1
            throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']);
1208
        }
1209 1430
    }
1210
1211
    /**
1212
     * Map a single embedded document.
1213
     */
1214 6
    public function mapOneEmbedded(array $mapping) : void
1215
    {
1216 6
        $mapping['embedded'] = true;
1217 6
        $mapping['type']     = 'one';
1218 6
        $this->mapField($mapping);
1219 5
    }
1220
1221
    /**
1222
     * Map a collection of embedded documents.
1223
     */
1224 5
    public function mapManyEmbedded(array $mapping) : void
1225
    {
1226 5
        $mapping['embedded'] = true;
1227 5
        $mapping['type']     = 'many';
1228 5
        $this->mapField($mapping);
1229 5
    }
1230
1231
    /**
1232
     * Map a single document reference.
1233
     */
1234 2
    public function mapOneReference(array $mapping) : void
1235
    {
1236 2
        $mapping['reference'] = true;
1237 2
        $mapping['type']      = 'one';
1238 2
        $this->mapField($mapping);
1239 2
    }
1240
1241
    /**
1242
     * Map a collection of document references.
1243
     */
1244 1
    public function mapManyReference(array $mapping) : void
1245
    {
1246 1
        $mapping['reference'] = true;
1247 1
        $mapping['type']      = 'many';
1248 1
        $this->mapField($mapping);
1249 1
    }
1250
1251
    /**
1252
     * INTERNAL:
1253
     * Adds a field mapping without completing/validating it.
1254
     * This is mainly used to add inherited field mappings to derived classes.
1255
     */
1256 139
    public function addInheritedFieldMapping(array $fieldMapping) : void
1257
    {
1258 139
        $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
1259
1260 139
        if (! isset($fieldMapping['association'])) {
1261 139
            return;
1262
        }
1263
1264 89
        $this->associationMappings[$fieldMapping['fieldName']] = $fieldMapping;
1265 89
    }
1266
1267
    /**
1268
     * INTERNAL:
1269
     * Adds an association mapping without completing/validating it.
1270
     * This is mainly used to add inherited association mappings to derived classes.
1271
     *
1272
     * @throws MappingException
1273
     */
1274 90
    public function addInheritedAssociationMapping(array $mapping) : void
1275
    {
1276 90
        $this->associationMappings[$mapping['fieldName']] = $mapping;
1277 90
    }
1278
1279
    /**
1280
     * Checks whether the class has a mapped association with the given field name.
1281
     */
1282 31
    public function hasReference(string $fieldName) : bool
1283
    {
1284 31
        return isset($this->fieldMappings[$fieldName]['reference']);
1285
    }
1286
1287
    /**
1288
     * Checks whether the class has a mapped embed with the given field name.
1289
     */
1290 4
    public function hasEmbed(string $fieldName) : bool
1291
    {
1292 4
        return isset($this->fieldMappings[$fieldName]['embedded']);
1293
    }
1294
1295
    /**
1296
     * {@inheritDoc}
1297
     *
1298
     * Checks whether the class has a mapped association (embed or reference) with the given field name.
1299
     */
1300 6
    public function hasAssociation($fieldName) : bool
1301
    {
1302 6
        return $this->hasReference($fieldName) || $this->hasEmbed($fieldName);
1303
    }
1304
1305
    /**
1306
     * {@inheritDoc}
1307
     *
1308
     * Checks whether the class has a mapped reference or embed for the specified field and
1309
     * is a single valued association.
1310
     */
1311
    public function isSingleValuedAssociation($fieldName) : bool
1312
    {
1313
        return $this->isSingleValuedReference($fieldName) || $this->isSingleValuedEmbed($fieldName);
1314
    }
1315
1316
    /**
1317
     * {@inheritDoc}
1318
     *
1319
     * Checks whether the class has a mapped reference or embed for the specified field and
1320
     * is a collection valued association.
1321
     */
1322
    public function isCollectionValuedAssociation($fieldName) : bool
1323
    {
1324
        return $this->isCollectionValuedReference($fieldName) || $this->isCollectionValuedEmbed($fieldName);
1325
    }
1326
1327
    /**
1328
     * Checks whether the class has a mapped association for the specified field
1329
     * and if yes, checks whether it is a single-valued association (to-one).
1330
     */
1331
    public function isSingleValuedReference(string $fieldName) : bool
1332
    {
1333
        return isset($this->fieldMappings[$fieldName]['association']) &&
1334
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_ONE;
1335
    }
1336
1337
    /**
1338
     * Checks whether the class has a mapped association for the specified field
1339
     * and if yes, checks whether it is a collection-valued association (to-many).
1340
     */
1341
    public function isCollectionValuedReference(string $fieldName) : bool
1342
    {
1343
        return isset($this->fieldMappings[$fieldName]['association']) &&
1344
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_MANY;
1345
    }
1346
1347
    /**
1348
     * Checks whether the class has a mapped embedded document for the specified field
1349
     * and if yes, checks whether it is a single-valued association (to-one).
1350
     */
1351
    public function isSingleValuedEmbed(string $fieldName) : bool
1352
    {
1353
        return isset($this->fieldMappings[$fieldName]['association']) &&
1354
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_ONE;
1355
    }
1356
1357
    /**
1358
     * Checks whether the class has a mapped embedded document for the specified field
1359
     * and if yes, checks whether it is a collection-valued association (to-many).
1360
     */
1361
    public function isCollectionValuedEmbed(string $fieldName) : bool
1362
    {
1363
        return isset($this->fieldMappings[$fieldName]['association']) &&
1364
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_MANY;
1365
    }
1366
1367
    /**
1368
     * Sets the ID generator used to generate IDs for instances of this class.
1369
     */
1370 1381
    public function setIdGenerator(AbstractIdGenerator $generator) : void
1371
    {
1372 1381
        $this->idGenerator = $generator;
1373 1381
    }
1374
1375
    /**
1376
     * Casts the identifier to its portable PHP type.
1377
     *
1378
     * @param mixed $id
1379
     *
1380
     * @return mixed $id
1381
     */
1382 644
    public function getPHPIdentifierValue($id)
1383
    {
1384 644
        $idType = $this->fieldMappings[$this->identifier]['type'];
1385 644
        return Type::getType($idType)->convertToPHPValue($id);
1386
    }
1387
1388
    /**
1389
     * Casts the identifier to its database type.
1390
     *
1391
     * @param mixed $id
1392
     *
1393
     * @return mixed $id
1394
     */
1395 708
    public function getDatabaseIdentifierValue($id)
1396
    {
1397 708
        $idType = $this->fieldMappings[$this->identifier]['type'];
1398 708
        return Type::getType($idType)->convertToDatabaseValue($id);
1399
    }
1400
1401
    /**
1402
     * Sets the document identifier of a document.
1403
     *
1404
     * The value will be converted to a PHP type before being set.
1405
     *
1406
     * @param mixed $id
1407
     */
1408 573
    public function setIdentifierValue(object $document, $id) : void
1409
    {
1410 573
        $id = $this->getPHPIdentifierValue($id);
1411 573
        $this->reflFields[$this->identifier]->setValue($document, $id);
1412 573
    }
1413
1414
    /**
1415
     * Gets the document identifier as a PHP type.
1416
     *
1417
     * @return mixed $id
1418
     */
1419 651
    public function getIdentifierValue(object $document)
1420
    {
1421 651
        return $this->reflFields[$this->identifier]->getValue($document);
1422
    }
1423
1424
    /**
1425
     * {@inheritDoc}
1426
     *
1427
     * Since MongoDB only allows exactly one identifier field this is a proxy
1428
     * to {@see getIdentifierValue()} and returns an array with the identifier
1429
     * field as a key.
1430
     */
1431
    public function getIdentifierValues($object) : array
1432
    {
1433
        return [$this->identifier => $this->getIdentifierValue($object)];
1434
    }
1435
1436
    /**
1437
     * Get the document identifier object as a database type.
1438
     *
1439
     * @return mixed $id
1440
     */
1441 30
    public function getIdentifierObject(object $document)
1442
    {
1443 30
        return $this->getDatabaseIdentifierValue($this->getIdentifierValue($document));
1444
    }
1445
1446
    /**
1447
     * Sets the specified field to the specified value on the given document.
1448
     *
1449
     * @param mixed $value
1450
     */
1451 8
    public function setFieldValue(object $document, string $field, $value) : void
1452
    {
1453 8
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
1454
            //property changes to an uninitialized proxy will not be tracked or persisted,
1455
            //so the proxy needs to be loaded first.
1456 1
            $document->initializeProxy();
1457
        }
1458
1459 8
        $this->reflFields[$field]->setValue($document, $value);
1460 8
    }
1461
1462
    /**
1463
     * Gets the specified field's value off the given document.
1464
     *
1465
     * @return mixed
1466
     */
1467 32
    public function getFieldValue(object $document, string $field)
1468
    {
1469 32
        if ($document instanceof GhostObjectInterface && $field !== $this->identifier && ! $document->isProxyInitialized()) {
1470 1
            $document->initializeProxy();
1471
        }
1472
1473 32
        return $this->reflFields[$field]->getValue($document);
1474
    }
1475
1476
    /**
1477
     * Gets the mapping of a field.
1478
     *
1479
     * @throws MappingException If the $fieldName is not found in the fieldMappings array.
1480
     */
1481 174
    public function getFieldMapping(string $fieldName) : array
1482
    {
1483 174
        if (! isset($this->fieldMappings[$fieldName])) {
1484 6
            throw MappingException::mappingNotFound($this->name, $fieldName);
1485
        }
1486 172
        return $this->fieldMappings[$fieldName];
1487
    }
1488
1489
    /**
1490
     * Gets mappings of fields holding embedded document(s).
1491
     */
1492 593
    public function getEmbeddedFieldsMappings() : array
1493
    {
1494 593
        return array_filter(
1495 593
            $this->associationMappings,
1496
            static function ($assoc) {
1497 455
                return ! empty($assoc['embedded']);
1498 593
            }
1499
        );
1500
    }
1501
1502
    /**
1503
     * Gets the field mapping by its DB name.
1504
     * E.g. it returns identifier's mapping when called with _id.
1505
     *
1506
     * @throws MappingException
1507
     */
1508 14
    public function getFieldMappingByDbFieldName(string $dbFieldName) : array
1509
    {
1510 14
        foreach ($this->fieldMappings as $mapping) {
1511 14
            if ($mapping['name'] === $dbFieldName) {
1512 14
                return $mapping;
1513
            }
1514
        }
1515
1516 1
        throw MappingException::mappingNotFoundByDbName($this->name, $dbFieldName);
1517
    }
1518
1519
    /**
1520
     * Check if the field is not null.
1521
     */
1522 1
    public function isNullable(string $fieldName) : bool
1523
    {
1524 1
        $mapping = $this->getFieldMapping($fieldName);
1525 1
        return isset($mapping['nullable']) && $mapping['nullable'] === true;
1526
    }
1527
1528
    /**
1529
     * Checks whether the document has a discriminator field and value configured.
1530
     */
1531 519
    public function hasDiscriminator() : bool
1532
    {
1533 519
        return isset($this->discriminatorField, $this->discriminatorValue);
1534
    }
1535
1536
    /**
1537
     * Sets the type of Id generator to use for the mapped class.
1538
     */
1539 913
    public function setIdGeneratorType(int $generatorType) : void
1540
    {
1541 913
        $this->generatorType = $generatorType;
1542 913
    }
1543
1544
    /**
1545
     * Sets the Id generator options.
1546
     */
1547
    public function setIdGeneratorOptions(array $generatorOptions) : void
1548
    {
1549
        $this->generatorOptions = $generatorOptions;
1550
    }
1551
1552 612
    public function isInheritanceTypeNone() : bool
1553
    {
1554 612
        return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
1555
    }
1556
1557
    /**
1558
     * Checks whether the mapped class uses the SINGLE_COLLECTION inheritance mapping strategy.
1559
     */
1560 912
    public function isInheritanceTypeSingleCollection() : bool
1561
    {
1562 912
        return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION;
1563
    }
1564
1565
    /**
1566
     * Checks whether the mapped class uses the COLLECTION_PER_CLASS inheritance mapping strategy.
1567
     */
1568
    public function isInheritanceTypeCollectionPerClass() : bool
1569
    {
1570
        return $this->inheritanceType === self::INHERITANCE_TYPE_COLLECTION_PER_CLASS;
1571
    }
1572
1573
    /**
1574
     * Sets the mapped subclasses of this class.
1575
     *
1576
     * @param string[] $subclasses The names of all mapped subclasses.
1577
     */
1578 2
    public function setSubclasses(array $subclasses) : void
1579
    {
1580 2
        foreach ($subclasses as $subclass) {
1581 2
            $this->subClasses[] = $subclass;
1582
        }
1583 2
    }
1584
1585
    /**
1586
     * Sets the parent class names.
1587
     * Assumes that the class names in the passed array are in the order:
1588
     * directParent -> directParentParent -> directParentParentParent ... -> root.
1589
     *
1590
     * @param string[] $classNames
1591
     */
1592 1436
    public function setParentClasses(array $classNames) : void
1593
    {
1594 1436
        $this->parentClasses = $classNames;
1595
1596 1436
        if (count($classNames) <= 0) {
1597 1435
            return;
1598
        }
1599
1600 123
        $this->rootDocumentName = (string) array_pop($classNames);
1601 123
    }
1602
1603
    /**
1604
     * Checks whether the class will generate a new \MongoDB\BSON\ObjectId instance for us.
1605
     */
1606
    public function isIdGeneratorAuto() : bool
1607
    {
1608
        return $this->generatorType === self::GENERATOR_TYPE_AUTO;
1609
    }
1610
1611
    /**
1612
     * Checks whether the class will use a collection to generate incremented identifiers.
1613
     */
1614
    public function isIdGeneratorIncrement() : bool
1615
    {
1616
        return $this->generatorType === self::GENERATOR_TYPE_INCREMENT;
1617
    }
1618
1619
    /**
1620
     * Checks whether the class will generate a uuid id.
1621
     */
1622
    public function isIdGeneratorUuid() : bool
1623
    {
1624
        return $this->generatorType === self::GENERATOR_TYPE_UUID;
1625
    }
1626
1627
    /**
1628
     * Checks whether the class uses no id generator.
1629
     */
1630
    public function isIdGeneratorNone() : bool
1631
    {
1632
        return $this->generatorType === self::GENERATOR_TYPE_NONE;
1633
    }
1634
1635
    /**
1636
     * Sets the version field mapping used for versioning. Sets the default
1637
     * value to use depending on the column type.
1638
     *
1639
     * @throws LockException
1640
     */
1641 108
    public function setVersionMapping(array &$mapping) : void
1642
    {
1643 108
        if ($mapping['type'] !== 'int' && $mapping['type'] !== 'date') {
1644 1
            throw LockException::invalidVersionFieldType($mapping['type']);
1645
        }
1646
1647 107
        $this->isVersioned  = true;
1648 107
        $this->versionField = $mapping['fieldName'];
1649 107
    }
1650
1651
    /**
1652
     * Sets whether this class is to be versioned for optimistic locking.
1653
     */
1654 913
    public function setVersioned(bool $bool) : void
1655
    {
1656 913
        $this->isVersioned = $bool;
1657 913
    }
1658
1659
    /**
1660
     * Sets the name of the field that is to be used for versioning if this class is
1661
     * versioned for optimistic locking.
1662
     */
1663 913
    public function setVersionField(?string $versionField) : void
1664
    {
1665 913
        $this->versionField = $versionField;
1666 913
    }
1667
1668
    /**
1669
     * Sets the version field mapping used for versioning. Sets the default
1670
     * value to use depending on the column type.
1671
     *
1672
     * @throws LockException
1673
     */
1674 22
    public function setLockMapping(array &$mapping) : void
1675
    {
1676 22
        if ($mapping['type'] !== 'int') {
1677 1
            throw LockException::invalidLockFieldType($mapping['type']);
1678
        }
1679
1680 21
        $this->isLockable = true;
1681 21
        $this->lockField  = $mapping['fieldName'];
1682 21
    }
1683
1684
    /**
1685
     * Sets whether this class is to allow pessimistic locking.
1686
     */
1687
    public function setLockable(bool $bool) : void
1688
    {
1689
        $this->isLockable = $bool;
1690
    }
1691
1692
    /**
1693
     * Sets the name of the field that is to be used for storing whether a document
1694
     * is currently locked or not.
1695
     */
1696
    public function setLockField(string $lockField) : void
1697
    {
1698
        $this->lockField = $lockField;
1699
    }
1700
1701
    /**
1702
     * Marks this class as read only, no change tracking is applied to it.
1703
     */
1704 5
    public function markReadOnly() : void
1705
    {
1706 5
        $this->isReadOnly = true;
1707 5
    }
1708
1709
    /**
1710
     * {@inheritDoc}
1711
     */
1712
    public function getFieldNames() : array
1713
    {
1714
        return array_keys($this->fieldMappings);
1715
    }
1716
1717
    /**
1718
     * {@inheritDoc}
1719
     */
1720
    public function getAssociationNames() : array
1721
    {
1722
        return array_keys($this->associationMappings);
1723
    }
1724
1725
    /**
1726
     * {@inheritDoc}
1727
     */
1728
    public function getTypeOfField($fieldName) : ?string
1729
    {
1730
        return isset($this->fieldMappings[$fieldName]) ?
1731
            $this->fieldMappings[$fieldName]['type'] : null;
1732
    }
1733
1734
    /**
1735
     * {@inheritDoc}
1736
     */
1737 4
    public function getAssociationTargetClass($assocName) : string
1738
    {
1739 4
        if (! isset($this->associationMappings[$assocName])) {
1740 2
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1741
        }
1742
1743 2
        return $this->associationMappings[$assocName]['targetDocument'];
1744
    }
1745
1746
    /**
1747
     * Retrieve the collectionClass associated with an association
1748
     */
1749
    public function getAssociationCollectionClass(string $assocName) : string
1750
    {
1751
        if (! isset($this->associationMappings[$assocName])) {
1752
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1753
        }
1754
1755
        if (! array_key_exists('collectionClass', $this->associationMappings[$assocName])) {
1756
            throw new InvalidArgumentException("collectionClass can only be applied to 'embedMany' and 'referenceMany' associations.");
1757
        }
1758
1759
        return $this->associationMappings[$assocName]['collectionClass'];
1760
    }
1761
1762
    /**
1763
     * {@inheritDoc}
1764
     */
1765
    public function isAssociationInverseSide($fieldName) : bool
1766
    {
1767
        throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1768
    }
1769
1770
    /**
1771
     * {@inheritDoc}
1772
     */
1773
    public function getAssociationMappedByTargetField($fieldName)
1774
    {
1775
        throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1776
    }
1777
1778
    /**
1779
     * Map a field.
1780
     *
1781
     * @throws MappingException
1782
     */
1783 1482
    public function mapField(array $mapping) : array
1784
    {
1785 1482
        if (! isset($mapping['fieldName']) && isset($mapping['name'])) {
1786 5
            $mapping['fieldName'] = $mapping['name'];
1787
        }
1788 1482
        if (! isset($mapping['fieldName']) || ! is_string($mapping['fieldName'])) {
1789
            throw MappingException::missingFieldName($this->name);
1790
        }
1791 1482
        if (! isset($mapping['name'])) {
1792 1481
            $mapping['name'] = $mapping['fieldName'];
1793
        }
1794
1795 1482
        if ($this->identifier === $mapping['name'] && empty($mapping['id'])) {
1796 1
            throw MappingException::mustNotChangeIdentifierFieldsType($this->name, (string) $mapping['name']);
1797
        }
1798 1481
        if ($this->discriminatorField !== null && $this->discriminatorField === $mapping['name']) {
1799 1
            throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField);
1800
        }
1801 1480
        if (isset($mapping['collectionClass'])) {
1802 72
            $mapping['collectionClass'] = ltrim($mapping['collectionClass'], '\\');
1803
        }
1804 1480
        if (! empty($mapping['collectionClass'])) {
1805 72
            $rColl = new ReflectionClass($mapping['collectionClass']);
1806 72
            if (! $rColl->implementsInterface('Doctrine\\Common\\Collections\\Collection')) {
1807 1
                throw MappingException::collectionClassDoesNotImplementCommonInterface($this->name, $mapping['fieldName'], $mapping['collectionClass']);
1808
            }
1809
        }
1810
1811 1479
        if (isset($mapping['cascade']) && isset($mapping['embedded'])) {
1812 1
            throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']);
1813
        }
1814
1815 1478
        $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : [];
1816
1817 1478
        if (in_array('all', $cascades) || isset($mapping['embedded'])) {
1818 1170
            $cascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
1819
        }
1820
1821 1478
        if (isset($mapping['embedded'])) {
1822 1131
            unset($mapping['cascade']);
1823 1473
        } elseif (isset($mapping['cascade'])) {
1824 939
            $mapping['cascade'] = $cascades;
1825
        }
1826
1827 1478
        $mapping['isCascadeRemove']  = in_array('remove', $cascades);
1828 1478
        $mapping['isCascadePersist'] = in_array('persist', $cascades);
1829 1478
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades);
1830 1478
        $mapping['isCascadeMerge']   = in_array('merge', $cascades);
1831 1478
        $mapping['isCascadeDetach']  = in_array('detach', $cascades);
1832
1833 1478
        if (isset($mapping['id']) && $mapping['id'] === true) {
1834 1444
            $mapping['name']  = '_id';
1835 1444
            $this->identifier = $mapping['fieldName'];
1836 1444
            if (isset($mapping['strategy'])) {
1837 1438
                $this->generatorType = constant(self::class . '::GENERATOR_TYPE_' . strtoupper($mapping['strategy']));
1838
            }
1839 1444
            $this->generatorOptions = $mapping['options'] ?? [];
1840 1444
            switch ($this->generatorType) {
1841 1444
                case self::GENERATOR_TYPE_AUTO:
1842 1372
                    $mapping['type'] = 'id';
1843 1372
                    break;
1844
                default:
1845 165
                    if (! empty($this->generatorOptions['type'])) {
1846 56
                        $mapping['type'] = $this->generatorOptions['type'];
1847 109
                    } elseif (empty($mapping['type'])) {
1848 97
                        $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? 'int_id' : 'custom_id';
1849
                    }
1850
            }
1851 1444
            unset($this->generatorOptions['type']);
1852
        }
1853
1854 1478
        if (! isset($mapping['nullable'])) {
1855 40
            $mapping['nullable'] = false;
1856
        }
1857
1858 1478
        if (isset($mapping['reference'])
1859 1478
            && isset($mapping['storeAs'])
1860 1478
            && $mapping['storeAs'] === self::REFERENCE_STORE_AS_ID
1861 1478
            && ! isset($mapping['targetDocument'])
1862
        ) {
1863 3
            throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']);
1864
        }
1865
1866 1475
        if (isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) &&
1867 1475
                (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))) {
1868 4
            throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']);
1869
        }
1870
1871 1471
        if ($this->isEmbeddedDocument && $mapping['type'] === 'many' && isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
1872 1
            throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']);
1873
        }
1874
1875 1470
        if (isset($mapping['repositoryMethod']) && ! (empty($mapping['skip']) && empty($mapping['limit']) && empty($mapping['sort']))) {
1876 3
            throw MappingException::repositoryMethodCanNotBeCombinedWithSkipLimitAndSort($this->name, $mapping['fieldName']);
1877
        }
1878
1879 1467
        if (isset($mapping['reference']) && $mapping['type'] === 'one') {
1880 1046
            $mapping['association'] = self::REFERENCE_ONE;
1881
        }
1882 1467
        if (isset($mapping['reference']) && $mapping['type'] === 'many') {
1883 990
            $mapping['association'] = self::REFERENCE_MANY;
1884
        }
1885 1467
        if (isset($mapping['embedded']) && $mapping['type'] === 'one') {
1886 986
            $mapping['association'] = self::EMBED_ONE;
1887
        }
1888 1467
        if (isset($mapping['embedded']) && $mapping['type'] === 'many') {
1889 1027
            $mapping['association'] = self::EMBED_MANY;
1890
        }
1891
1892 1467
        if (isset($mapping['association']) && ! isset($mapping['targetDocument']) && ! isset($mapping['discriminatorField'])) {
1893 144
            $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD;
1894
        }
1895
1896 1467
        if (isset($mapping['version'])) {
1897 108
            $mapping['notSaved'] = true;
1898 108
            $this->setVersionMapping($mapping);
1899
        }
1900 1467
        if (isset($mapping['lock'])) {
1901 22
            $mapping['notSaved'] = true;
1902 22
            $this->setLockMapping($mapping);
1903
        }
1904 1467
        $mapping['isOwningSide']  = true;
1905 1467
        $mapping['isInverseSide'] = false;
1906 1467
        if (isset($mapping['reference'])) {
1907 1113
            if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
1908 104
                $mapping['isOwningSide']  = true;
1909 104
                $mapping['isInverseSide'] = false;
1910
            }
1911 1113
            if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
1912 838
                $mapping['isInverseSide'] = true;
1913 838
                $mapping['isOwningSide']  = false;
1914
            }
1915 1113
            if (isset($mapping['repositoryMethod'])) {
1916 78
                $mapping['isInverseSide'] = true;
1917 78
                $mapping['isOwningSide']  = false;
1918
            }
1919 1113
            if (! isset($mapping['orphanRemoval'])) {
1920 1094
                $mapping['orphanRemoval'] = false;
1921
            }
1922
        }
1923
1924 1467
        if (! empty($mapping['prime']) && ($mapping['association'] !== self::REFERENCE_MANY || ! $mapping['isInverseSide'])) {
1925
            throw MappingException::referencePrimersOnlySupportedForInverseReferenceMany($this->name, $mapping['fieldName']);
1926
        }
1927
1928 1467
        if ($this->isFile && ! $this->isAllowedGridFSField($mapping['name'])) {
1929 1
            throw MappingException::fieldNotAllowedForGridFS($this->name, $mapping['fieldName']);
1930
        }
1931
1932 1466
        $this->applyStorageStrategy($mapping);
1933 1465
        $this->checkDuplicateMapping($mapping);
1934
1935 1465
        $this->fieldMappings[$mapping['fieldName']] = $mapping;
1936 1465
        if (isset($mapping['association'])) {
1937 1268
            $this->associationMappings[$mapping['fieldName']] = $mapping;
1938
        }
1939
1940 1465
        $reflProp = $this->reflClass->getProperty($mapping['fieldName']);
1941 1464
        $reflProp->setAccessible(true);
1942 1464
        $this->reflFields[$mapping['fieldName']] = $reflProp;
1943
1944 1464
        return $mapping;
1945
    }
1946
1947
    /**
1948
     * Determines which fields get serialized.
1949
     *
1950
     * It is only serialized what is necessary for best unserialization performance.
1951
     * That means any metadata properties that are not set or empty or simply have
1952
     * their default value are NOT serialized.
1953
     *
1954
     * Parts that are also NOT serialized because they can not be properly unserialized:
1955
     *      - reflClass (ReflectionClass)
1956
     *      - reflFields (ReflectionProperty array)
1957
     *
1958
     * @return array The names of all the fields that should be serialized.
1959
     */
1960 6
    public function __sleep()
1961
    {
1962
        // This metadata is always serialized/cached.
1963
        $serialized = [
1964 6
            'fieldMappings',
1965
            'associationMappings',
1966
            'identifier',
1967
            'name',
1968
            'db',
1969
            'collection',
1970
            'readPreference',
1971
            'readPreferenceTags',
1972
            'writeConcern',
1973
            'rootDocumentName',
1974
            'generatorType',
1975
            'generatorOptions',
1976
            'idGenerator',
1977
            'indexes',
1978
            'shardKey',
1979
        ];
1980
1981
        // The rest of the metadata is only serialized if necessary.
1982 6
        if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
1983
            $serialized[] = 'changeTrackingPolicy';
1984
        }
1985
1986 6
        if ($this->customRepositoryClassName) {
1987 1
            $serialized[] = 'customRepositoryClassName';
1988
        }
1989
1990 6
        if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE || $this->discriminatorField !== null) {
1991 4
            $serialized[] = 'inheritanceType';
1992 4
            $serialized[] = 'discriminatorField';
1993 4
            $serialized[] = 'discriminatorValue';
1994 4
            $serialized[] = 'discriminatorMap';
1995 4
            $serialized[] = 'defaultDiscriminatorValue';
1996 4
            $serialized[] = 'parentClasses';
1997 4
            $serialized[] = 'subClasses';
1998
        }
1999
2000 6
        if ($this->isMappedSuperclass) {
2001 1
            $serialized[] = 'isMappedSuperclass';
2002
        }
2003
2004 6
        if ($this->isEmbeddedDocument) {
2005 1
            $serialized[] = 'isEmbeddedDocument';
2006
        }
2007
2008 6
        if ($this->isQueryResultDocument) {
2009
            $serialized[] = 'isQueryResultDocument';
2010
        }
2011
2012 6
        if ($this->isFile) {
2013
            $serialized[] = 'isFile';
2014
            $serialized[] = 'bucketName';
2015
            $serialized[] = 'chunkSizeBytes';
2016
        }
2017
2018 6
        if ($this->isVersioned) {
2019
            $serialized[] = 'isVersioned';
2020
            $serialized[] = 'versionField';
2021
        }
2022
2023 6
        if ($this->lifecycleCallbacks) {
2024
            $serialized[] = 'lifecycleCallbacks';
2025
        }
2026
2027 6
        if ($this->collectionCapped) {
2028 1
            $serialized[] = 'collectionCapped';
2029 1
            $serialized[] = 'collectionSize';
2030 1
            $serialized[] = 'collectionMax';
2031
        }
2032
2033 6
        if ($this->isReadOnly) {
2034
            $serialized[] = 'isReadOnly';
2035
        }
2036
2037 6
        return $serialized;
2038
    }
2039
2040
    /**
2041
     * Restores some state that can not be serialized/unserialized.
2042
     */
2043 6
    public function __wakeup()
2044
    {
2045
        // Restore ReflectionClass and properties
2046 6
        $this->reflClass    = new ReflectionClass($this->name);
2047 6
        $this->instantiator = new Instantiator();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Doctrine\Instantiator\Instantiator() of type object<Doctrine\Instantiator\Instantiator> is incompatible with the declared type object<Doctrine\Instanti...\InstantiatorInterface> of property $instantiator.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2048
2049 6
        foreach ($this->fieldMappings as $field => $mapping) {
2050 3
            if (isset($mapping['declared'])) {
2051 1
                $reflField = new ReflectionProperty($mapping['declared'], $field);
2052
            } else {
2053 3
                $reflField = $this->reflClass->getProperty($field);
2054
            }
2055 3
            $reflField->setAccessible(true);
2056 3
            $this->reflFields[$field] = $reflField;
2057
        }
2058 6
    }
2059
2060
    /**
2061
     * Creates a new instance of the mapped class, without invoking the constructor.
2062
     */
2063 367
    public function newInstance() : object
2064
    {
2065 367
        return $this->instantiator->instantiate($this->name);
2066
    }
2067
2068 81
    private function isAllowedGridFSField(string $name) : bool
2069
    {
2070 81
        return in_array($name, self::ALLOWED_GRIDFS_FIELDS, true);
2071
    }
2072
2073 1465
    private function checkDuplicateMapping(array $mapping) : void
2074
    {
2075 1465
        if ($mapping['notSaved'] ?? false) {
2076 867
            return;
2077
        }
2078
2079 1465
        foreach ($this->fieldMappings as $fieldName => $otherMapping) {
2080
            // Ignore fields with the same name - we can safely override their mapping
2081 1419
            if ($mapping['fieldName'] === $fieldName) {
2082 72
                continue;
2083
            }
2084
2085
            // Ignore fields with a different name in the database
2086 1415
            if ($mapping['name'] !== $otherMapping['name']) {
2087 1415
                continue;
2088
            }
2089
2090
            // If the other field is not saved, ignore it as well
2091 2
            if ($otherMapping['notSaved'] ?? false) {
2092
                continue;
2093
            }
2094
2095 2
            throw MappingException::duplicateDatabaseFieldName($this->getName(), $mapping['fieldName'], $mapping['name'], $fieldName);
2096
        }
2097 1465
    }
2098
}
2099