Completed
Push — master ( 48b0b5...26ecbc )
by Andreas
16:25 queued 10s
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 call_user_func_array;
26
use function class_exists;
27
use function constant;
28
use function count;
29
use function get_class;
30
use function in_array;
31
use function is_array;
32
use function is_string;
33
use function is_subclass_of;
34
use function ltrim;
35
use function sprintf;
36
use function strtolower;
37
use function strtoupper;
38
39
/**
40
 * A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata
41
 * of a document and it's references.
42
 *
43
 * Once populated, ClassMetadata instances are usually cached in a serialized form.
44
 *
45
 * <b>IMPORTANT NOTE:</b>
46
 *
47
 * The fields of this class are only public for 2 reasons:
48
 * 1) To allow fast READ access.
49
 * 2) To drastically reduce the size of a serialized instance (private/protected members
50
 *    get the whole class name, namespace inclusive, prepended to every property in
51
 *    the serialized representation).
52
 */
53
class ClassMetadata implements BaseClassMetadata
54
{
55
    /* The Id generator types. */
56
    /**
57
     * AUTO means Doctrine will automatically create a new \MongoDB\BSON\ObjectId instance for us.
58
     */
59
    public const GENERATOR_TYPE_AUTO = 1;
60
61
    /**
62
     * INCREMENT means a separate collection is used for maintaining and incrementing id generation.
63
     * Offers full portability.
64
     */
65
    public const GENERATOR_TYPE_INCREMENT = 2;
66
67
    /**
68
     * UUID means Doctrine will generate a uuid for us.
69
     */
70
    public const GENERATOR_TYPE_UUID = 3;
71
72
    /**
73
     * ALNUM means Doctrine will generate Alpha-numeric string identifiers, using the INCREMENT
74
     * generator to ensure identifier uniqueness
75
     */
76
    public const GENERATOR_TYPE_ALNUM = 4;
77
78
    /**
79
     * CUSTOM means Doctrine expect a class parameter. It will then try to initiate that class
80
     * and pass other options to the generator. It will throw an Exception if the class
81
     * does not exist or if an option was passed for that there is not setter in the new
82
     * generator class.
83
     *
84
     * The class  will have to be a subtype of AbstractIdGenerator.
85
     */
86
    public const GENERATOR_TYPE_CUSTOM = 5;
87
88
    /**
89
     * NONE means Doctrine will not generate any id for us and you are responsible for manually
90
     * assigning an id.
91
     */
92
    public const GENERATOR_TYPE_NONE = 6;
93
94
    /**
95
     * Default discriminator field name.
96
     *
97
     * This is used for associations value for associations where a that do not define a "targetDocument" or
98
     * "discriminatorField" option in their mapping.
99
     */
100
    public const DEFAULT_DISCRIMINATOR_FIELD = '_doctrine_class_name';
101
102
    public const REFERENCE_ONE  = 1;
103
    public const REFERENCE_MANY = 2;
104
    public const EMBED_ONE      = 3;
105
    public const EMBED_MANY     = 4;
106
    public const MANY           = 'many';
107
    public const ONE            = 'one';
108
109
    /**
110
     * The types of storeAs references
111
     */
112
    public const REFERENCE_STORE_AS_ID             = 'id';
113
    public const REFERENCE_STORE_AS_DB_REF         = 'dbRef';
114
    public const REFERENCE_STORE_AS_DB_REF_WITH_DB = 'dbRefWithDb';
115
    public const REFERENCE_STORE_AS_REF            = 'ref';
116
117
    /* The inheritance mapping types */
118
    /**
119
     * NONE means the class does not participate in an inheritance hierarchy
120
     * and therefore does not need an inheritance mapping type.
121
     */
122
    public const INHERITANCE_TYPE_NONE = 1;
123
124
    /**
125
     * SINGLE_COLLECTION means the class will be persisted according to the rules of
126
     * <tt>Single Collection Inheritance</tt>.
127
     */
128
    public const INHERITANCE_TYPE_SINGLE_COLLECTION = 2;
129
130
    /**
131
     * COLLECTION_PER_CLASS means the class will be persisted according to the rules
132
     * of <tt>Concrete Collection Inheritance</tt>.
133
     */
134
    public const INHERITANCE_TYPE_COLLECTION_PER_CLASS = 3;
135
136
    /**
137
     * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
138
     * by doing a property-by-property comparison with the original data. This will
139
     * be done for all entities that are in MANAGED state at commit-time.
140
     *
141
     * This is the default change tracking policy.
142
     */
143
    public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
144
145
    /**
146
     * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
147
     * by doing a property-by-property comparison with the original data. This will
148
     * be done only for entities that were explicitly saved (through persist() or a cascade).
149
     */
150
    public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
151
152
    /**
153
     * NOTIFY means that Doctrine relies on the entities sending out notifications
154
     * when their properties change. Such entity classes must implement
155
     * the <tt>NotifyPropertyChanged</tt> interface.
156
     */
157
    public const CHANGETRACKING_NOTIFY = 3;
158
159
    /**
160
     * SET means that fields will be written to the database using a $set operator
161
     */
162
    public const STORAGE_STRATEGY_SET = 'set';
163
164
    /**
165
     * INCREMENT means that fields will be written to the database by calculating
166
     * the difference and using the $inc operator
167
     */
168
    public const STORAGE_STRATEGY_INCREMENT = 'increment';
169
170
    public const STORAGE_STRATEGY_PUSH_ALL         = 'pushAll';
171
    public const STORAGE_STRATEGY_ADD_TO_SET       = 'addToSet';
172
    public const STORAGE_STRATEGY_ATOMIC_SET       = 'atomicSet';
173
    public const STORAGE_STRATEGY_ATOMIC_SET_ARRAY = 'atomicSetArray';
174
    public const STORAGE_STRATEGY_SET_ARRAY        = 'setArray';
175
176
    private const ALLOWED_GRIDFS_FIELDS = ['_id', 'chunkSize', 'filename', 'length', 'metadata', 'uploadDate'];
177
178
    /**
179
     * READ-ONLY: The name of the mongo database the document is mapped to.
180
     *
181
     * @var string|null
182
     */
183
    public $db;
184
185
    /**
186
     * READ-ONLY: The name of the mongo collection the document is mapped to.
187
     *
188
     * @var string
189
     */
190
    public $collection;
191
192
    /**
193
     * READ-ONLY: The name of the GridFS bucket the document is mapped to.
194
     *
195
     * @var string
196
     */
197
    public $bucketName = 'fs';
198
199
    /**
200
     * READ-ONLY: If the collection should be a fixed size.
201
     *
202
     * @var bool
203
     */
204
    public $collectionCapped = false;
205
206
    /**
207
     * READ-ONLY: If the collection is fixed size, its size in bytes.
208
     *
209
     * @var int|null
210
     */
211
    public $collectionSize;
212
213
    /**
214
     * READ-ONLY: If the collection is fixed size, the maximum number of elements to store in the collection.
215
     *
216
     * @var int|null
217
     */
218
    public $collectionMax;
219
220
    /**
221
     * READ-ONLY Describes how MongoDB clients route read operations to the members of a replica set.
222
     *
223
     * @var string|int|null
224
     */
225
    public $readPreference;
226
227
    /**
228
     * READ-ONLY Associated with readPreference Allows to specify criteria so that your application can target read
229
     * operations to specific members, based on custom parameters.
230
     *
231
     * @var string[][]|null
232
     */
233
    public $readPreferenceTags;
234
235
    /**
236
     * READ-ONLY: Describes the level of acknowledgement requested from MongoDB for write operations.
237
     *
238
     * @var string|int|null
239
     */
240
    public $writeConcern;
241
242
    /**
243
     * READ-ONLY: The field name of the document identifier.
244
     *
245
     * @var string|null
246
     */
247
    public $identifier;
248
249
    /**
250
     * READ-ONLY: The array of indexes for the document collection.
251
     *
252
     * @var array
253
     */
254
    public $indexes = [];
255
256
    /**
257
     * READ-ONLY: Keys and options describing shard key. Only for sharded collections.
258
     *
259
     * @var string|null
260
     */
261
    public $shardKey;
262
263
    /**
264
     * READ-ONLY: The name of the document class.
265
     *
266
     * @var string
267
     */
268
    public $name;
269
270
    /**
271
     * READ-ONLY: The name of the document class that is at the root of the mapped document inheritance
272
     * hierarchy. If the document is not part of a mapped inheritance hierarchy this is the same
273
     * as {@link $documentName}.
274
     *
275
     * @var string
276
     */
277
    public $rootDocumentName;
278
279
    /**
280
     * The name of the custom repository class used for the document class.
281
     * (Optional).
282
     *
283
     * @var string|null
284
     */
285
    public $customRepositoryClassName;
286
287
    /**
288
     * READ-ONLY: The names of the parent classes (ancestors).
289
     *
290
     * @var array
291
     */
292
    public $parentClasses = [];
293
294
    /**
295
     * READ-ONLY: The names of all subclasses (descendants).
296
     *
297
     * @var array
298
     */
299
    public $subClasses = [];
300
301
    /**
302
     * The ReflectionProperty instances of the mapped class.
303
     *
304
     * @var ReflectionProperty[]
305
     */
306
    public $reflFields = [];
307
308
    /**
309
     * READ-ONLY: The inheritance mapping type used by the class.
310
     *
311
     * @var int
312
     */
313
    public $inheritanceType = self::INHERITANCE_TYPE_NONE;
314
315
    /**
316
     * READ-ONLY: The Id generator type used by the class.
317
     *
318
     * @var int
319
     */
320
    public $generatorType = self::GENERATOR_TYPE_AUTO;
321
322
    /**
323
     * READ-ONLY: The Id generator options.
324
     *
325
     * @var array
326
     */
327
    public $generatorOptions = [];
328
329
    /**
330
     * READ-ONLY: The ID generator used for generating IDs for this class.
331
     *
332
     * @var AbstractIdGenerator
333
     */
334
    public $idGenerator;
335
336
    /**
337
     * READ-ONLY: The field mappings of the class.
338
     * Keys are field names and values are mapping definitions.
339
     *
340
     * The mapping definition array has the following values:
341
     *
342
     * - <b>fieldName</b> (string)
343
     * The name of the field in the Document.
344
     *
345
     * - <b>id</b> (boolean, optional)
346
     * Marks the field as the primary key of the document. Multiple fields of an
347
     * document can have the id attribute, forming a composite key.
348
     *
349
     * @var array
350
     */
351
    public $fieldMappings = [];
352
353
    /**
354
     * READ-ONLY: The association mappings of the class.
355
     * Keys are field names and values are mapping definitions.
356
     *
357
     * @var array
358
     */
359
    public $associationMappings = [];
360
361
    /**
362
     * READ-ONLY: Array of fields to also load with a given method.
363
     *
364
     * @var array
365
     */
366
    public $alsoLoadMethods = [];
367
368
    /**
369
     * READ-ONLY: The registered lifecycle callbacks for documents of this class.
370
     *
371
     * @var array
372
     */
373
    public $lifecycleCallbacks = [];
374
375
    /**
376
     * READ-ONLY: The discriminator value of this class.
377
     *
378
     * <b>This does only apply to the JOINED and SINGLE_COLLECTION inheritance mapping strategies
379
     * where a discriminator field is used.</b>
380
     *
381
     * @see discriminatorField
382
     *
383
     * @var mixed
384
     */
385
    public $discriminatorValue;
386
387
    /**
388
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
389
     *
390
     * <b>This does only apply to the SINGLE_COLLECTION inheritance mapping strategy
391
     * where a discriminator field is used.</b>
392
     *
393
     * @see discriminatorField
394
     *
395
     * @var mixed
396
     */
397
    public $discriminatorMap = [];
398
399
    /**
400
     * READ-ONLY: The definition of the discriminator field used in SINGLE_COLLECTION
401
     * inheritance mapping.
402
     *
403
     * @var string
404
     */
405
    public $discriminatorField;
406
407
    /**
408
     * READ-ONLY: The default value for discriminatorField in case it's not set in the document
409
     *
410
     * @see discriminatorField
411
     *
412
     * @var string
413
     */
414
    public $defaultDiscriminatorValue;
415
416
    /**
417
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
418
     *
419
     * @var bool
420
     */
421
    public $isMappedSuperclass = false;
422
423
    /**
424
     * READ-ONLY: Whether this class describes the mapping of a embedded document.
425
     *
426
     * @var bool
427
     */
428
    public $isEmbeddedDocument = false;
429
430
    /**
431
     * READ-ONLY: Whether this class describes the mapping of an aggregation result document.
432
     *
433
     * @var bool
434
     */
435
    public $isQueryResultDocument = false;
436
437
    /**
438
     * READ-ONLY: Whether this class describes the mapping of a gridFS file
439
     *
440
     * @var bool
441
     */
442
    public $isFile = false;
443
444
    /**
445
     * READ-ONLY: The default chunk size in bytes for the file
446
     *
447
     * @var int|null
448
     */
449
    public $chunkSizeBytes;
450
451
    /**
452
     * READ-ONLY: The policy used for change-tracking on entities of this class.
453
     *
454
     * @var int
455
     */
456
    public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
457
458
    /**
459
     * READ-ONLY: A flag for whether or not instances of this class are to be versioned
460
     * with optimistic locking.
461
     *
462
     * @var bool $isVersioned
463
     */
464
    public $isVersioned = false;
465
466
    /**
467
     * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
468
     *
469
     * @var string|null $versionField
470
     */
471
    public $versionField;
472
473
    /**
474
     * READ-ONLY: A flag for whether or not instances of this class are to allow pessimistic
475
     * locking.
476
     *
477
     * @var bool $isLockable
478
     */
479
    public $isLockable = false;
480
481
    /**
482
     * READ-ONLY: The name of the field which is used for locking a document.
483
     *
484
     * @var mixed $lockField
485
     */
486
    public $lockField;
487
488
    /**
489
     * The ReflectionClass instance of the mapped class.
490
     *
491
     * @var ReflectionClass
492
     */
493
    public $reflClass;
494
495
    /**
496
     * READ_ONLY: A flag for whether or not this document is read-only.
497
     *
498
     * @var bool
499
     */
500
    public $isReadOnly;
501
502
    /** @var InstantiatorInterface|null */
503
    private $instantiator;
504
505
    /**
506
     * Initializes a new ClassMetadata instance that will hold the object-document mapping
507
     * metadata of the class with the given name.
508
     */
509 1564
    public function __construct(string $documentName)
510
    {
511 1564
        $this->name             = $documentName;
512 1564
        $this->rootDocumentName = $documentName;
513 1564
        $this->reflClass        = new ReflectionClass($documentName);
514 1564
        $this->setCollection($this->reflClass->getShortName());
515 1564
        $this->instantiator = new Instantiator();
516 1564
    }
517
518
    /**
519
     * Helper method to get reference id of ref* type references
520
     *
521
     * @internal
522
     *
523
     * @param mixed $reference
524
     *
525
     * @return mixed
526
     */
527 116
    public static function getReferenceId($reference, string $storeAs)
528
    {
529 116
        return $storeAs === self::REFERENCE_STORE_AS_ID ? $reference : $reference[self::getReferencePrefix($storeAs) . 'id'];
530
    }
531
532
    /**
533
     * Returns the reference prefix used for a reference
534
     */
535 184
    private static function getReferencePrefix(string $storeAs) : string
536
    {
537 184
        if (! in_array($storeAs, [self::REFERENCE_STORE_AS_REF, self::REFERENCE_STORE_AS_DB_REF, self::REFERENCE_STORE_AS_DB_REF_WITH_DB])) {
538
            throw new LogicException('Can only get a reference prefix for DBRef and reference arrays');
539
        }
540
541 184
        return $storeAs === self::REFERENCE_STORE_AS_REF ? '' : '$';
542
    }
543
544
    /**
545
     * Returns a fully qualified field name for a given reference
546
     *
547
     * @internal
548
     *
549
     * @param string $pathPrefix The field path prefix
550
     */
551 137
    public static function getReferenceFieldName(string $storeAs, string $pathPrefix = '') : string
552
    {
553 137
        if ($storeAs === self::REFERENCE_STORE_AS_ID) {
554 97
            return $pathPrefix;
555
        }
556
557 125
        return ($pathPrefix ? $pathPrefix . '.' : '') . static::getReferencePrefix($storeAs) . 'id';
558
    }
559
560
    /**
561
     * {@inheritDoc}
562
     */
563 1448
    public function getReflectionClass() : ReflectionClass
564
    {
565 1448
        if (! $this->reflClass) {
566
            $this->reflClass = new ReflectionClass($this->name);
567
        }
568
569 1448
        return $this->reflClass;
570
    }
571
572
    /**
573
     * {@inheritDoc}
574
     */
575 323
    public function isIdentifier($fieldName) : bool
576
    {
577 323
        return $this->identifier === $fieldName;
578
    }
579
580
    /**
581
     * INTERNAL:
582
     * Sets the mapped identifier field of this class.
583
     */
584 914
    public function setIdentifier(?string $identifier) : void
585
    {
586 914
        $this->identifier = $identifier;
587 914
    }
588
589
    /**
590
     * {@inheritDoc}
591
     *
592
     * Since MongoDB only allows exactly one identifier field
593
     * this will always return an array with only one value
594
     */
595 11
    public function getIdentifier() : array
596
    {
597 11
        return [$this->identifier];
598
    }
599
600
    /**
601
     * {@inheritDoc}
602
     *
603
     * Since MongoDB only allows exactly one identifier field
604
     * this will always return an array with only one value
605
     */
606 99
    public function getIdentifierFieldNames() : array
607
    {
608 99
        return [$this->identifier];
609
    }
610
611
    /**
612
     * {@inheritDoc}
613
     */
614 918
    public function hasField($fieldName) : bool
615
    {
616 918
        return isset($this->fieldMappings[$fieldName]);
617
    }
618
619
    /**
620
     * Sets the inheritance type used by the class and it's subclasses.
621
     */
622 930
    public function setInheritanceType(int $type) : void
623
    {
624 930
        $this->inheritanceType = $type;
625 930
    }
626
627
    /**
628
     * Checks whether a mapped field is inherited from an entity superclass.
629
     */
630 1439
    public function isInheritedField(string $fieldName) : bool
631
    {
632 1439
        return isset($this->fieldMappings[$fieldName]['inherited']);
633
    }
634
635
    /**
636
     * Registers a custom repository class for the document class.
637
     */
638 861
    public function setCustomRepositoryClass(?string $repositoryClassName) : void
639
    {
640 861
        if ($this->isEmbeddedDocument || $this->isQueryResultDocument) {
641
            return;
642
        }
643
644 861
        $this->customRepositoryClassName = $repositoryClassName;
645 861
    }
646
647
    /**
648
     * Dispatches the lifecycle event of the given document by invoking all
649
     * registered callbacks.
650
     *
651
     * @throws InvalidArgumentException If document class is not this class or
652
     *                                   a Proxy of this class.
653
     */
654 645
    public function invokeLifecycleCallbacks(string $event, object $document, ?array $arguments = null) : void
655
    {
656 645
        if (! $document instanceof $this->name) {
657 1
            throw new InvalidArgumentException(sprintf('Expected document class "%s"; found: "%s"', $this->name, get_class($document)));
658
        }
659
660 644
        if (empty($this->lifecycleCallbacks[$event])) {
661 629
            return;
662
        }
663
664 189
        foreach ($this->lifecycleCallbacks[$event] as $callback) {
665 189
            if ($arguments !== null) {
666 188
                call_user_func_array([$document, $callback], $arguments);
667
            } else {
668 189
                $document->$callback();
669
            }
670
        }
671 189
    }
672
673
    /**
674
     * Checks whether the class has callbacks registered for a lifecycle event.
675
     */
676
    public function hasLifecycleCallbacks(string $event) : bool
677
    {
678
        return ! empty($this->lifecycleCallbacks[$event]);
679
    }
680
681
    /**
682
     * Gets the registered lifecycle callbacks for an event.
683
     */
684
    public function getLifecycleCallbacks(string $event) : array
685
    {
686
        return $this->lifecycleCallbacks[$event] ?? [];
687
    }
688
689
    /**
690
     * Adds a lifecycle callback for documents of this class.
691
     *
692
     * If the callback is already registered, this is a NOOP.
693
     */
694 832
    public function addLifecycleCallback(string $callback, string $event) : void
695
    {
696 832
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
697 1
            return;
698
        }
699
700 832
        $this->lifecycleCallbacks[$event][] = $callback;
701 832
    }
702
703
    /**
704
     * Sets the lifecycle callbacks for documents of this class.
705
     *
706
     * Any previously registered callbacks are overwritten.
707
     */
708 913
    public function setLifecycleCallbacks(array $callbacks) : void
709
    {
710 913
        $this->lifecycleCallbacks = $callbacks;
711 913
    }
712
713
    /**
714
     * Registers a method for loading document data before field hydration.
715
     *
716
     * Note: A method may be registered multiple times for different fields.
717
     * it will be invoked only once for the first field found.
718
     *
719
     * @param array|string $fields Database field name(s)
720
     */
721 14
    public function registerAlsoLoadMethod(string $method, $fields) : void
722
    {
723 14
        $this->alsoLoadMethods[$method] = is_array($fields) ? $fields : [$fields];
724 14
    }
725
726
    /**
727
     * Sets the AlsoLoad methods for documents of this class.
728
     *
729
     * Any previously registered methods are overwritten.
730
     */
731 913
    public function setAlsoLoadMethods(array $methods) : void
732
    {
733 913
        $this->alsoLoadMethods = $methods;
734 913
    }
735
736
    /**
737
     * Sets the discriminator field.
738
     *
739
     * The field name is the the unmapped database field. Discriminator values
740
     * are only used to discern the hydration class and are not mapped to class
741
     * properties.
742
     *
743
     * @param string|array $discriminatorField
744
     *
745
     * @throws MappingException If the discriminator field conflicts with the
746
     *                          "name" attribute of a mapped field.
747
     */
748 939
    public function setDiscriminatorField($discriminatorField) : void
749
    {
750 939
        if ($this->isFile) {
751
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
752
        }
753
754 939
        if ($discriminatorField === null) {
755 870
            $this->discriminatorField = null;
756
757 870
            return;
758
        }
759
760
        // Handle array argument with name/fieldName keys for BC
761 138
        if (is_array($discriminatorField)) {
762
            if (isset($discriminatorField['name'])) {
763
                $discriminatorField = $discriminatorField['name'];
764
            } elseif (isset($discriminatorField['fieldName'])) {
765
                $discriminatorField = $discriminatorField['fieldName'];
766
            }
767
        }
768
769 138
        foreach ($this->fieldMappings as $fieldMapping) {
770 4
            if ($discriminatorField === $fieldMapping['name']) {
771 4
                throw MappingException::discriminatorFieldConflict($this->name, $discriminatorField);
772
            }
773
        }
774
775 137
        $this->discriminatorField = $discriminatorField;
776 137
    }
777
778
    /**
779
     * Sets the discriminator values used by this class.
780
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
781
     *
782
     * @throws MappingException
783
     */
784 932
    public function setDiscriminatorMap(array $map) : void
785
    {
786 932
        if ($this->isFile) {
787
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
788
        }
789
790 932
        foreach ($map as $value => $className) {
791 132
            $this->discriminatorMap[$value] = $className;
792 132
            if ($this->name === $className) {
793 124
                $this->discriminatorValue = $value;
794
            } else {
795 131
                if (! class_exists($className)) {
796
                    throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
797
                }
798 131
                if (is_subclass_of($className, $this->name)) {
799 132
                    $this->subClasses[] = $className;
800
                }
801
            }
802
        }
803 932
    }
804
805
    /**
806
     * Sets the default discriminator value to be used for this class
807
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies if the document has no discriminator value
808
     *
809
     * @throws MappingException
810
     */
811 916
    public function setDefaultDiscriminatorValue(?string $defaultDiscriminatorValue) : void
812
    {
813 916
        if ($this->isFile) {
814
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
815
        }
816
817 916
        if ($defaultDiscriminatorValue === null) {
818 913
            $this->defaultDiscriminatorValue = null;
819
820 913
            return;
821
        }
822
823 68
        if (! array_key_exists($defaultDiscriminatorValue, $this->discriminatorMap)) {
824
            throw MappingException::invalidDiscriminatorValue($defaultDiscriminatorValue, $this->name);
825
        }
826
827 68
        $this->defaultDiscriminatorValue = $defaultDiscriminatorValue;
828 68
    }
829
830
    /**
831
     * Sets the discriminator value for this class.
832
     * Used for JOINED/SINGLE_TABLE inheritance and multiple document types in a single
833
     * collection.
834
     *
835
     * @throws MappingException
836
     */
837 3
    public function setDiscriminatorValue(string $value) : void
838
    {
839 3
        if ($this->isFile) {
840
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
841
        }
842
843 3
        $this->discriminatorMap[$value] = $this->name;
844 3
        $this->discriminatorValue       = $value;
845 3
    }
846
847
    /**
848
     * Add a index for this Document.
849
     */
850 216
    public function addIndex(array $keys, array $options = []) : void
851
    {
852 216
        $this->indexes[] = [
853
            'keys' => array_map(static function ($value) {
854 216
                if ($value === 1 || $value === -1) {
855 65
                    return (int) $value;
856
                }
857 216
                if (is_string($value)) {
858 216
                    $lower = strtolower($value);
859 216
                    if ($lower === 'asc') {
860 209
                        return 1;
861
                    }
862
863 72
                    if ($lower === 'desc') {
864
                        return -1;
865
                    }
866
                }
867 72
                return $value;
868 216
            }, $keys),
869 216
            'options' => $options,
870
        ];
871 216
    }
872
873
    /**
874
     * Returns the array of indexes for this Document.
875
     */
876 26
    public function getIndexes() : array
877
    {
878 26
        return $this->indexes;
879
    }
880
881
    /**
882
     * Checks whether this document has indexes or not.
883
     */
884
    public function hasIndexes() : bool
885
    {
886
        return $this->indexes ? true : false;
887
    }
888
889
    /**
890
     * Set shard key for this Document.
891
     *
892
     * @throws MappingException
893
     */
894 100
    public function setShardKey(array $keys, array $options = []) : void
895
    {
896 100
        if ($this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION && $this->shardKey !== null) {
897 2
            throw MappingException::shardKeyInSingleCollInheritanceSubclass($this->getName());
898
        }
899
900 100
        if ($this->isEmbeddedDocument) {
901 2
            throw MappingException::embeddedDocumentCantHaveShardKey($this->getName());
902
        }
903
904 98
        foreach (array_keys($keys) as $field) {
905 98
            if (! isset($this->fieldMappings[$field])) {
906 91
                continue;
907
            }
908
909 7
            if (in_array($this->fieldMappings[$field]['type'], ['many', 'collection'])) {
910 3
                throw MappingException::noMultiKeyShardKeys($this->getName(), $field);
911
            }
912
913 4
            if ($this->fieldMappings[$field]['strategy'] !== static::STORAGE_STRATEGY_SET) {
914 4
                throw MappingException::onlySetStrategyAllowedInShardKey($this->getName(), $field);
915
            }
916
        }
917
918 94
        $this->shardKey = [
919
            'keys' => array_map(static function ($value) {
920 94
                if ($value === 1 || $value === -1) {
921 5
                    return (int) $value;
922
                }
923 94
                if (is_string($value)) {
924 94
                    $lower = strtolower($value);
925 94
                    if ($lower === 'asc') {
926 92
                        return 1;
927
                    }
928
929 67
                    if ($lower === 'desc') {
930
                        return -1;
931
                    }
932
                }
933 67
                return $value;
934 94
            }, $keys),
935 94
            'options' => $options,
936
        ];
937 94
    }
938
939 26
    public function getShardKey() : array
940
    {
941 26
        return $this->shardKey;
942
    }
943
944
    /**
945
     * Checks whether this document has shard key or not.
946
     */
947 1159
    public function isSharded() : bool
948
    {
949 1159
        return $this->shardKey ? true : false;
950
    }
951
952
    /**
953
     * Sets the read preference used by this class.
954
     *
955
     * @param string|int|null $readPreference
956
     * @param array|null      $tags
957
     */
958 913
    public function setReadPreference($readPreference, $tags) : void
959
    {
960 913
        $this->readPreference     = $readPreference;
961 913
        $this->readPreferenceTags = $tags;
962 913
    }
963
964
    /**
965
     * Sets the write concern used by this class.
966
     *
967
     * @param string|int|null $writeConcern
968
     */
969 923
    public function setWriteConcern($writeConcern) : void
970
    {
971 923
        $this->writeConcern = $writeConcern;
972 923
    }
973
974
    /**
975
     * @return int|string|null
976
     */
977 11
    public function getWriteConcern()
978
    {
979 11
        return $this->writeConcern;
980
    }
981
982
    /**
983
     * Whether there is a write concern configured for this class.
984
     */
985 592
    public function hasWriteConcern() : bool
986
    {
987 592
        return $this->writeConcern !== null;
988
    }
989
990
    /**
991
     * Sets the change tracking policy used by this class.
992
     */
993 915
    public function setChangeTrackingPolicy(int $policy) : void
994
    {
995 915
        $this->changeTrackingPolicy = $policy;
996 915
    }
997
998
    /**
999
     * Whether the change tracking policy of this class is "deferred explicit".
1000
     */
1001 68
    public function isChangeTrackingDeferredExplicit() : bool
1002
    {
1003 68
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
1004
    }
1005
1006
    /**
1007
     * Whether the change tracking policy of this class is "deferred implicit".
1008
     */
1009 615
    public function isChangeTrackingDeferredImplicit() : bool
1010
    {
1011 615
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
1012
    }
1013
1014
    /**
1015
     * Whether the change tracking policy of this class is "notify".
1016
     */
1017 342
    public function isChangeTrackingNotify() : bool
1018
    {
1019 342
        return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
1020
    }
1021
1022
    /**
1023
     * Gets the ReflectionProperties of the mapped class.
1024
     */
1025 1
    public function getReflectionProperties() : array
1026
    {
1027 1
        return $this->reflFields;
1028
    }
1029
1030
    /**
1031
     * Gets a ReflectionProperty for a specific field of the mapped class.
1032
     */
1033 98
    public function getReflectionProperty(string $name) : ReflectionProperty
1034
    {
1035 98
        return $this->reflFields[$name];
1036
    }
1037
1038
    /**
1039
     * {@inheritDoc}
1040
     */
1041 1447
    public function getName() : string
1042
    {
1043 1447
        return $this->name;
1044
    }
1045
1046
    /**
1047
     * Returns the database this Document is mapped to.
1048
     */
1049 1366
    public function getDatabase() : ?string
1050
    {
1051 1366
        return $this->db;
1052
    }
1053
1054
    /**
1055
     * Set the database this Document is mapped to.
1056
     */
1057 111
    public function setDatabase(?string $db) : void
1058
    {
1059 111
        $this->db = $db;
1060 111
    }
1061
1062
    /**
1063
     * Get the collection this Document is mapped to.
1064
     */
1065 1358
    public function getCollection() : string
1066
    {
1067 1358
        return $this->collection;
1068
    }
1069
1070
    /**
1071
     * Sets the collection this Document is mapped to.
1072
     *
1073
     * @param array|string $name
1074
     *
1075
     * @throws InvalidArgumentException
1076
     */
1077 1564
    public function setCollection($name) : void
1078
    {
1079 1564
        if (is_array($name)) {
1080 1
            if (! isset($name['name'])) {
1081
                throw new InvalidArgumentException('A name key is required when passing an array to setCollection()');
1082
            }
1083 1
            $this->collectionCapped = $name['capped'] ?? false;
1084 1
            $this->collectionSize   = $name['size'] ?? 0;
1085 1
            $this->collectionMax    = $name['max'] ?? 0;
1086 1
            $this->collection       = $name['name'];
1087
        } else {
1088 1564
            $this->collection = $name;
1089
        }
1090 1564
    }
1091
1092 19
    public function getBucketName() : ?string
1093
    {
1094 19
        return $this->bucketName;
1095
    }
1096
1097 1
    public function setBucketName(string $bucketName) : void
1098
    {
1099 1
        $this->bucketName = $bucketName;
1100 1
        $this->setCollection($bucketName . '.files');
1101 1
    }
1102
1103 10
    public function getChunkSizeBytes() : ?int
1104
    {
1105 10
        return $this->chunkSizeBytes;
1106
    }
1107
1108 79
    public function setChunkSizeBytes(int $chunkSizeBytes) : void
1109
    {
1110 79
        $this->chunkSizeBytes = $chunkSizeBytes;
1111 79
    }
1112
1113
    /**
1114
     * Get whether or not the documents collection is capped.
1115
     */
1116 5
    public function getCollectionCapped() : bool
1117
    {
1118 5
        return $this->collectionCapped;
1119
    }
1120
1121
    /**
1122
     * Set whether or not the documents collection is capped.
1123
     */
1124 1
    public function setCollectionCapped(bool $bool) : void
1125
    {
1126 1
        $this->collectionCapped = $bool;
1127 1
    }
1128
1129
    /**
1130
     * Get the collection size
1131
     */
1132 5
    public function getCollectionSize() : ?int
1133
    {
1134 5
        return $this->collectionSize;
1135
    }
1136
1137
    /**
1138
     * Set the collection size.
1139
     */
1140 1
    public function setCollectionSize(int $size) : void
1141
    {
1142 1
        $this->collectionSize = $size;
1143 1
    }
1144
1145
    /**
1146
     * Get the collection max.
1147
     */
1148 5
    public function getCollectionMax() : ?int
1149
    {
1150 5
        return $this->collectionMax;
1151
    }
1152
1153
    /**
1154
     * Set the collection max.
1155
     */
1156 1
    public function setCollectionMax(int $max) : void
1157
    {
1158 1
        $this->collectionMax = $max;
1159 1
    }
1160
1161
    /**
1162
     * Returns TRUE if this Document is mapped to a collection FALSE otherwise.
1163
     */
1164
    public function isMappedToCollection() : bool
1165
    {
1166
        return $this->collection ? true : false;
1167
    }
1168
1169
    /**
1170
     * Validates the storage strategy of a mapping for consistency
1171
     *
1172
     * @throws MappingException
1173
     */
1174 1466
    private function applyStorageStrategy(array &$mapping) : void
1175
    {
1176 1466
        if (! isset($mapping['type']) || isset($mapping['id'])) {
1177 1446
            return;
1178
        }
1179
1180
        switch (true) {
1181 1431
            case $mapping['type'] === 'int':
1182 1430
            case $mapping['type'] === 'float':
1183 879
                $defaultStrategy   = self::STORAGE_STRATEGY_SET;
1184 879
                $allowedStrategies = [self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_INCREMENT];
1185 879
                break;
1186
1187 1430
            case $mapping['type'] === 'many':
1188 1126
                $defaultStrategy   = CollectionHelper::DEFAULT_STRATEGY;
1189
                $allowedStrategies = [
1190 1126
                    self::STORAGE_STRATEGY_PUSH_ALL,
1191 1126
                    self::STORAGE_STRATEGY_ADD_TO_SET,
1192 1126
                    self::STORAGE_STRATEGY_SET,
1193 1126
                    self::STORAGE_STRATEGY_SET_ARRAY,
1194 1126
                    self::STORAGE_STRATEGY_ATOMIC_SET,
1195 1126
                    self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY,
1196
                ];
1197 1126
                break;
1198
1199
            default:
1200 1418
                $defaultStrategy   = self::STORAGE_STRATEGY_SET;
1201 1418
                $allowedStrategies = [self::STORAGE_STRATEGY_SET];
1202
        }
1203
1204 1431
        if (! isset($mapping['strategy'])) {
1205 1423
            $mapping['strategy'] = $defaultStrategy;
1206
        }
1207
1208 1431
        if (! in_array($mapping['strategy'], $allowedStrategies)) {
1209
            throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']);
1210
        }
1211
1212 1431
        if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide']
1213 1431
            && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])) {
1214 1
            throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']);
1215
        }
1216 1430
    }
1217
1218
    /**
1219
     * Map a single embedded document.
1220
     */
1221 6
    public function mapOneEmbedded(array $mapping) : void
1222
    {
1223 6
        $mapping['embedded'] = true;
1224 6
        $mapping['type']     = 'one';
1225 6
        $this->mapField($mapping);
1226 5
    }
1227
1228
    /**
1229
     * Map a collection of embedded documents.
1230
     */
1231 5
    public function mapManyEmbedded(array $mapping) : void
1232
    {
1233 5
        $mapping['embedded'] = true;
1234 5
        $mapping['type']     = 'many';
1235 5
        $this->mapField($mapping);
1236 5
    }
1237
1238
    /**
1239
     * Map a single document reference.
1240
     */
1241 2
    public function mapOneReference(array $mapping) : void
1242
    {
1243 2
        $mapping['reference'] = true;
1244 2
        $mapping['type']      = 'one';
1245 2
        $this->mapField($mapping);
1246 2
    }
1247
1248
    /**
1249
     * Map a collection of document references.
1250
     */
1251 1
    public function mapManyReference(array $mapping) : void
1252
    {
1253 1
        $mapping['reference'] = true;
1254 1
        $mapping['type']      = 'many';
1255 1
        $this->mapField($mapping);
1256 1
    }
1257
1258
    /**
1259
     * INTERNAL:
1260
     * Adds a field mapping without completing/validating it.
1261
     * This is mainly used to add inherited field mappings to derived classes.
1262
     */
1263 139
    public function addInheritedFieldMapping(array $fieldMapping) : void
1264
    {
1265 139
        $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
1266
1267 139
        if (! isset($fieldMapping['association'])) {
1268 139
            return;
1269
        }
1270
1271 89
        $this->associationMappings[$fieldMapping['fieldName']] = $fieldMapping;
1272 89
    }
1273
1274
    /**
1275
     * INTERNAL:
1276
     * Adds an association mapping without completing/validating it.
1277
     * This is mainly used to add inherited association mappings to derived classes.
1278
     *
1279
     * @throws MappingException
1280
     */
1281 90
    public function addInheritedAssociationMapping(array $mapping) : void
1282
    {
1283 90
        $this->associationMappings[$mapping['fieldName']] = $mapping;
1284 90
    }
1285
1286
    /**
1287
     * Checks whether the class has a mapped association with the given field name.
1288
     */
1289 31
    public function hasReference(string $fieldName) : bool
1290
    {
1291 31
        return isset($this->fieldMappings[$fieldName]['reference']);
1292
    }
1293
1294
    /**
1295
     * Checks whether the class has a mapped embed with the given field name.
1296
     */
1297 4
    public function hasEmbed(string $fieldName) : bool
1298
    {
1299 4
        return isset($this->fieldMappings[$fieldName]['embedded']);
1300
    }
1301
1302
    /**
1303
     * {@inheritDoc}
1304
     *
1305
     * Checks whether the class has a mapped association (embed or reference) with the given field name.
1306
     */
1307 6
    public function hasAssociation($fieldName) : bool
1308
    {
1309 6
        return $this->hasReference($fieldName) || $this->hasEmbed($fieldName);
1310
    }
1311
1312
    /**
1313
     * {@inheritDoc}
1314
     *
1315
     * Checks whether the class has a mapped reference or embed for the specified field and
1316
     * is a single valued association.
1317
     */
1318
    public function isSingleValuedAssociation($fieldName) : bool
1319
    {
1320
        return $this->isSingleValuedReference($fieldName) || $this->isSingleValuedEmbed($fieldName);
1321
    }
1322
1323
    /**
1324
     * {@inheritDoc}
1325
     *
1326
     * Checks whether the class has a mapped reference or embed for the specified field and
1327
     * is a collection valued association.
1328
     */
1329
    public function isCollectionValuedAssociation($fieldName) : bool
1330
    {
1331
        return $this->isCollectionValuedReference($fieldName) || $this->isCollectionValuedEmbed($fieldName);
1332
    }
1333
1334
    /**
1335
     * Checks whether the class has a mapped association for the specified field
1336
     * and if yes, checks whether it is a single-valued association (to-one).
1337
     */
1338
    public function isSingleValuedReference(string $fieldName) : bool
1339
    {
1340
        return isset($this->fieldMappings[$fieldName]['association']) &&
1341
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_ONE;
1342
    }
1343
1344
    /**
1345
     * Checks whether the class has a mapped association for the specified field
1346
     * and if yes, checks whether it is a collection-valued association (to-many).
1347
     */
1348
    public function isCollectionValuedReference(string $fieldName) : bool
1349
    {
1350
        return isset($this->fieldMappings[$fieldName]['association']) &&
1351
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_MANY;
1352
    }
1353
1354
    /**
1355
     * Checks whether the class has a mapped embedded document for the specified field
1356
     * and if yes, checks whether it is a single-valued association (to-one).
1357
     */
1358
    public function isSingleValuedEmbed(string $fieldName) : bool
1359
    {
1360
        return isset($this->fieldMappings[$fieldName]['association']) &&
1361
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_ONE;
1362
    }
1363
1364
    /**
1365
     * Checks whether the class has a mapped embedded document for the specified field
1366
     * and if yes, checks whether it is a collection-valued association (to-many).
1367
     */
1368
    public function isCollectionValuedEmbed(string $fieldName) : bool
1369
    {
1370
        return isset($this->fieldMappings[$fieldName]['association']) &&
1371
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_MANY;
1372
    }
1373
1374
    /**
1375
     * Sets the ID generator used to generate IDs for instances of this class.
1376
     */
1377 1381
    public function setIdGenerator(AbstractIdGenerator $generator) : void
1378
    {
1379 1381
        $this->idGenerator = $generator;
1380 1381
    }
1381
1382
    /**
1383
     * Casts the identifier to its portable PHP type.
1384
     *
1385
     * @param mixed $id
1386
     *
1387
     * @return mixed $id
1388
     */
1389 644
    public function getPHPIdentifierValue($id)
1390
    {
1391 644
        $idType = $this->fieldMappings[$this->identifier]['type'];
1392 644
        return Type::getType($idType)->convertToPHPValue($id);
1393
    }
1394
1395
    /**
1396
     * Casts the identifier to its database type.
1397
     *
1398
     * @param mixed $id
1399
     *
1400
     * @return mixed $id
1401
     */
1402 708
    public function getDatabaseIdentifierValue($id)
1403
    {
1404 708
        $idType = $this->fieldMappings[$this->identifier]['type'];
1405 708
        return Type::getType($idType)->convertToDatabaseValue($id);
1406
    }
1407
1408
    /**
1409
     * Sets the document identifier of a document.
1410
     *
1411
     * The value will be converted to a PHP type before being set.
1412
     *
1413
     * @param mixed $id
1414
     */
1415 573
    public function setIdentifierValue(object $document, $id) : void
1416
    {
1417 573
        $id = $this->getPHPIdentifierValue($id);
1418 573
        $this->reflFields[$this->identifier]->setValue($document, $id);
1419 573
    }
1420
1421
    /**
1422
     * Gets the document identifier as a PHP type.
1423
     *
1424
     * @return mixed $id
1425
     */
1426 651
    public function getIdentifierValue(object $document)
1427
    {
1428 651
        return $this->reflFields[$this->identifier]->getValue($document);
1429
    }
1430
1431
    /**
1432
     * {@inheritDoc}
1433
     *
1434
     * Since MongoDB only allows exactly one identifier field this is a proxy
1435
     * to {@see getIdentifierValue()} and returns an array with the identifier
1436
     * field as a key.
1437
     */
1438
    public function getIdentifierValues($object) : array
1439
    {
1440
        return [$this->identifier => $this->getIdentifierValue($object)];
1441
    }
1442
1443
    /**
1444
     * Get the document identifier object as a database type.
1445
     *
1446
     * @return mixed $id
1447
     */
1448 30
    public function getIdentifierObject(object $document)
1449
    {
1450 30
        return $this->getDatabaseIdentifierValue($this->getIdentifierValue($document));
1451
    }
1452
1453
    /**
1454
     * Sets the specified field to the specified value on the given document.
1455
     *
1456
     * @param mixed $value
1457
     */
1458 8
    public function setFieldValue(object $document, string $field, $value) : void
1459
    {
1460 8
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
1461
            //property changes to an uninitialized proxy will not be tracked or persisted,
1462
            //so the proxy needs to be loaded first.
1463 1
            $document->initializeProxy();
1464
        }
1465
1466 8
        $this->reflFields[$field]->setValue($document, $value);
1467 8
    }
1468
1469
    /**
1470
     * Gets the specified field's value off the given document.
1471
     *
1472
     * @return mixed
1473
     */
1474 32
    public function getFieldValue(object $document, string $field)
1475
    {
1476 32
        if ($document instanceof GhostObjectInterface && $field !== $this->identifier && ! $document->isProxyInitialized()) {
1477 1
            $document->initializeProxy();
1478
        }
1479
1480 32
        return $this->reflFields[$field]->getValue($document);
1481
    }
1482
1483
    /**
1484
     * Gets the mapping of a field.
1485
     *
1486
     * @throws MappingException If the $fieldName is not found in the fieldMappings array.
1487
     */
1488 174
    public function getFieldMapping(string $fieldName) : array
1489
    {
1490 174
        if (! isset($this->fieldMappings[$fieldName])) {
1491 6
            throw MappingException::mappingNotFound($this->name, $fieldName);
1492
        }
1493 172
        return $this->fieldMappings[$fieldName];
1494
    }
1495
1496
    /**
1497
     * Gets mappings of fields holding embedded document(s).
1498
     */
1499 593
    public function getEmbeddedFieldsMappings() : array
1500
    {
1501 593
        return array_filter(
1502 593
            $this->associationMappings,
1503
            static function ($assoc) {
1504 455
                return ! empty($assoc['embedded']);
1505 593
            }
1506
        );
1507
    }
1508
1509
    /**
1510
     * Gets the field mapping by its DB name.
1511
     * E.g. it returns identifier's mapping when called with _id.
1512
     *
1513
     * @throws MappingException
1514
     */
1515 14
    public function getFieldMappingByDbFieldName(string $dbFieldName) : array
1516
    {
1517 14
        foreach ($this->fieldMappings as $mapping) {
1518 14
            if ($mapping['name'] === $dbFieldName) {
1519 14
                return $mapping;
1520
            }
1521
        }
1522
1523 1
        throw MappingException::mappingNotFoundByDbName($this->name, $dbFieldName);
1524
    }
1525
1526
    /**
1527
     * Check if the field is not null.
1528
     */
1529 1
    public function isNullable(string $fieldName) : bool
1530
    {
1531 1
        $mapping = $this->getFieldMapping($fieldName);
1532 1
        if ($mapping !== false) {
1533 1
            return isset($mapping['nullable']) && $mapping['nullable'] === true;
1534
        }
1535
        return false;
1536
    }
1537
1538
    /**
1539
     * Checks whether the document has a discriminator field and value configured.
1540
     */
1541 519
    public function hasDiscriminator() : bool
1542
    {
1543 519
        return isset($this->discriminatorField, $this->discriminatorValue);
1544
    }
1545
1546
    /**
1547
     * Sets the type of Id generator to use for the mapped class.
1548
     */
1549 913
    public function setIdGeneratorType(int $generatorType) : void
1550
    {
1551 913
        $this->generatorType = $generatorType;
1552 913
    }
1553
1554
    /**
1555
     * Sets the Id generator options.
1556
     */
1557
    public function setIdGeneratorOptions(array $generatorOptions) : void
1558
    {
1559
        $this->generatorOptions = $generatorOptions;
1560
    }
1561
1562 612
    public function isInheritanceTypeNone() : bool
1563
    {
1564 612
        return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
1565
    }
1566
1567
    /**
1568
     * Checks whether the mapped class uses the SINGLE_COLLECTION inheritance mapping strategy.
1569
     */
1570 912
    public function isInheritanceTypeSingleCollection() : bool
1571
    {
1572 912
        return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION;
1573
    }
1574
1575
    /**
1576
     * Checks whether the mapped class uses the COLLECTION_PER_CLASS inheritance mapping strategy.
1577
     */
1578
    public function isInheritanceTypeCollectionPerClass() : bool
1579
    {
1580
        return $this->inheritanceType === self::INHERITANCE_TYPE_COLLECTION_PER_CLASS;
1581
    }
1582
1583
    /**
1584
     * Sets the mapped subclasses of this class.
1585
     *
1586
     * @param string[] $subclasses The names of all mapped subclasses.
1587
     */
1588 2
    public function setSubclasses(array $subclasses) : void
1589
    {
1590 2
        foreach ($subclasses as $subclass) {
1591 2
            $this->subClasses[] = $subclass;
1592
        }
1593 2
    }
1594
1595
    /**
1596
     * Sets the parent class names.
1597
     * Assumes that the class names in the passed array are in the order:
1598
     * directParent -> directParentParent -> directParentParentParent ... -> root.
1599
     *
1600
     * @param string[] $classNames
1601
     */
1602 1436
    public function setParentClasses(array $classNames) : void
1603
    {
1604 1436
        $this->parentClasses = $classNames;
1605
1606 1436
        if (count($classNames) <= 0) {
1607 1435
            return;
1608
        }
1609
1610 123
        $this->rootDocumentName = array_pop($classNames);
1611 123
    }
1612
1613
    /**
1614
     * Checks whether the class will generate a new \MongoDB\BSON\ObjectId instance for us.
1615
     */
1616
    public function isIdGeneratorAuto() : bool
1617
    {
1618
        return $this->generatorType === self::GENERATOR_TYPE_AUTO;
1619
    }
1620
1621
    /**
1622
     * Checks whether the class will use a collection to generate incremented identifiers.
1623
     */
1624
    public function isIdGeneratorIncrement() : bool
1625
    {
1626
        return $this->generatorType === self::GENERATOR_TYPE_INCREMENT;
1627
    }
1628
1629
    /**
1630
     * Checks whether the class will generate a uuid id.
1631
     */
1632
    public function isIdGeneratorUuid() : bool
1633
    {
1634
        return $this->generatorType === self::GENERATOR_TYPE_UUID;
1635
    }
1636
1637
    /**
1638
     * Checks whether the class uses no id generator.
1639
     */
1640
    public function isIdGeneratorNone() : bool
1641
    {
1642
        return $this->generatorType === self::GENERATOR_TYPE_NONE;
1643
    }
1644
1645
    /**
1646
     * Sets the version field mapping used for versioning. Sets the default
1647
     * value to use depending on the column type.
1648
     *
1649
     * @throws LockException
1650
     */
1651 108
    public function setVersionMapping(array &$mapping) : void
1652
    {
1653 108
        if ($mapping['type'] !== 'int' && $mapping['type'] !== 'date') {
1654 1
            throw LockException::invalidVersionFieldType($mapping['type']);
1655
        }
1656
1657 107
        $this->isVersioned  = true;
1658 107
        $this->versionField = $mapping['fieldName'];
1659 107
    }
1660
1661
    /**
1662
     * Sets whether this class is to be versioned for optimistic locking.
1663
     */
1664 913
    public function setVersioned(bool $bool) : void
1665
    {
1666 913
        $this->isVersioned = $bool;
1667 913
    }
1668
1669
    /**
1670
     * Sets the name of the field that is to be used for versioning if this class is
1671
     * versioned for optimistic locking.
1672
     */
1673 913
    public function setVersionField(?string $versionField) : void
1674
    {
1675 913
        $this->versionField = $versionField;
1676 913
    }
1677
1678
    /**
1679
     * Sets the version field mapping used for versioning. Sets the default
1680
     * value to use depending on the column type.
1681
     *
1682
     * @throws LockException
1683
     */
1684 22
    public function setLockMapping(array &$mapping) : void
1685
    {
1686 22
        if ($mapping['type'] !== 'int') {
1687 1
            throw LockException::invalidLockFieldType($mapping['type']);
1688
        }
1689
1690 21
        $this->isLockable = true;
1691 21
        $this->lockField  = $mapping['fieldName'];
1692 21
    }
1693
1694
    /**
1695
     * Sets whether this class is to allow pessimistic locking.
1696
     */
1697
    public function setLockable(bool $bool) : void
1698
    {
1699
        $this->isLockable = $bool;
1700
    }
1701
1702
    /**
1703
     * Sets the name of the field that is to be used for storing whether a document
1704
     * is currently locked or not.
1705
     */
1706
    public function setLockField(string $lockField) : void
1707
    {
1708
        $this->lockField = $lockField;
1709
    }
1710
1711
    /**
1712
     * Marks this class as read only, no change tracking is applied to it.
1713
     */
1714 5
    public function markReadOnly() : void
1715
    {
1716 5
        $this->isReadOnly = true;
1717 5
    }
1718
1719
    /**
1720
     * {@inheritDoc}
1721
     */
1722
    public function getFieldNames() : array
1723
    {
1724
        return array_keys($this->fieldMappings);
1725
    }
1726
1727
    /**
1728
     * {@inheritDoc}
1729
     */
1730
    public function getAssociationNames() : array
1731
    {
1732
        return array_keys($this->associationMappings);
1733
    }
1734
1735
    /**
1736
     * {@inheritDoc}
1737
     */
1738
    public function getTypeOfField($fieldName) : ?string
1739
    {
1740
        return isset($this->fieldMappings[$fieldName]) ?
1741
            $this->fieldMappings[$fieldName]['type'] : null;
1742
    }
1743
1744
    /**
1745
     * {@inheritDoc}
1746
     */
1747 4
    public function getAssociationTargetClass($assocName) : string
1748
    {
1749 4
        if (! isset($this->associationMappings[$assocName])) {
1750 2
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1751
        }
1752
1753 2
        return $this->associationMappings[$assocName]['targetDocument'];
1754
    }
1755
1756
    /**
1757
     * Retrieve the collectionClass associated with an association
1758
     */
1759
    public function getAssociationCollectionClass(string $assocName) : string
1760
    {
1761
        if (! isset($this->associationMappings[$assocName])) {
1762
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1763
        }
1764
1765
        if (! array_key_exists('collectionClass', $this->associationMappings[$assocName])) {
1766
            throw new InvalidArgumentException("collectionClass can only be applied to 'embedMany' and 'referenceMany' associations.");
1767
        }
1768
1769
        return $this->associationMappings[$assocName]['collectionClass'];
1770
    }
1771
1772
    /**
1773
     * {@inheritDoc}
1774
     */
1775
    public function isAssociationInverseSide($fieldName) : bool
1776
    {
1777
        throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1778
    }
1779
1780
    /**
1781
     * {@inheritDoc}
1782
     */
1783
    public function getAssociationMappedByTargetField($fieldName)
1784
    {
1785
        throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1786
    }
1787
1788
    /**
1789
     * Map a field.
1790
     *
1791
     * @throws MappingException
1792
     */
1793 1482
    public function mapField(array $mapping) : array
1794
    {
1795 1482
        if (! isset($mapping['fieldName']) && isset($mapping['name'])) {
1796 5
            $mapping['fieldName'] = $mapping['name'];
1797
        }
1798 1482
        if (! isset($mapping['fieldName'])) {
1799
            throw MappingException::missingFieldName($this->name);
1800
        }
1801 1482
        if (! isset($mapping['name'])) {
1802 1481
            $mapping['name'] = $mapping['fieldName'];
1803
        }
1804 1482
        if ($this->identifier === $mapping['name'] && empty($mapping['id'])) {
1805 1
            throw MappingException::mustNotChangeIdentifierFieldsType($this->name, $mapping['name']);
1806
        }
1807 1481
        if ($this->discriminatorField !== null && $this->discriminatorField === $mapping['name']) {
1808 1
            throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField);
1809
        }
1810 1480
        if (isset($mapping['collectionClass'])) {
1811 72
            $mapping['collectionClass'] = ltrim($mapping['collectionClass'], '\\');
1812
        }
1813 1480
        if (! empty($mapping['collectionClass'])) {
1814 72
            $rColl = new ReflectionClass($mapping['collectionClass']);
1815 72
            if (! $rColl->implementsInterface('Doctrine\\Common\\Collections\\Collection')) {
1816 1
                throw MappingException::collectionClassDoesNotImplementCommonInterface($this->name, $mapping['fieldName'], $mapping['collectionClass']);
1817
            }
1818
        }
1819
1820 1479
        if (isset($mapping['cascade']) && isset($mapping['embedded'])) {
1821 1
            throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']);
1822
        }
1823
1824 1478
        $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : [];
1825
1826 1478
        if (in_array('all', $cascades) || isset($mapping['embedded'])) {
1827 1170
            $cascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
1828
        }
1829
1830 1478
        if (isset($mapping['embedded'])) {
1831 1131
            unset($mapping['cascade']);
1832 1473
        } elseif (isset($mapping['cascade'])) {
1833 939
            $mapping['cascade'] = $cascades;
1834
        }
1835
1836 1478
        $mapping['isCascadeRemove']  = in_array('remove', $cascades);
1837 1478
        $mapping['isCascadePersist'] = in_array('persist', $cascades);
1838 1478
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades);
1839 1478
        $mapping['isCascadeMerge']   = in_array('merge', $cascades);
1840 1478
        $mapping['isCascadeDetach']  = in_array('detach', $cascades);
1841
1842 1478
        if (isset($mapping['id']) && $mapping['id'] === true) {
1843 1444
            $mapping['name']  = '_id';
1844 1444
            $this->identifier = $mapping['fieldName'];
1845 1444
            if (isset($mapping['strategy'])) {
1846 1438
                $this->generatorType = constant(self::class . '::GENERATOR_TYPE_' . strtoupper($mapping['strategy']));
1847
            }
1848 1444
            $this->generatorOptions = $mapping['options'] ?? [];
1849 1444
            switch ($this->generatorType) {
1850 1444
                case self::GENERATOR_TYPE_AUTO:
1851 1372
                    $mapping['type'] = 'id';
1852 1372
                    break;
1853
                default:
1854 165
                    if (! empty($this->generatorOptions['type'])) {
1855 56
                        $mapping['type'] = $this->generatorOptions['type'];
1856 109
                    } elseif (empty($mapping['type'])) {
1857 97
                        $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? 'int_id' : 'custom_id';
1858
                    }
1859
            }
1860 1444
            unset($this->generatorOptions['type']);
1861
        }
1862
1863 1478
        if (! isset($mapping['nullable'])) {
1864 40
            $mapping['nullable'] = false;
1865
        }
1866
1867 1478
        if (isset($mapping['reference'])
1868 1478
            && isset($mapping['storeAs'])
1869 1478
            && $mapping['storeAs'] === self::REFERENCE_STORE_AS_ID
1870 1478
            && ! isset($mapping['targetDocument'])
1871
        ) {
1872 3
            throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']);
1873
        }
1874
1875 1475
        if (isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) &&
1876 1475
                (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))) {
1877 4
            throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']);
1878
        }
1879
1880 1471
        if ($this->isEmbeddedDocument && $mapping['type'] === 'many' && isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
1881 1
            throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']);
1882
        }
1883
1884 1470
        if (isset($mapping['repositoryMethod']) && ! (empty($mapping['skip']) && empty($mapping['limit']) && empty($mapping['sort']))) {
1885 3
            throw MappingException::repositoryMethodCanNotBeCombinedWithSkipLimitAndSort($this->name, $mapping['fieldName']);
1886
        }
1887
1888 1467
        if (isset($mapping['reference']) && $mapping['type'] === 'one') {
1889 1046
            $mapping['association'] = self::REFERENCE_ONE;
1890
        }
1891 1467
        if (isset($mapping['reference']) && $mapping['type'] === 'many') {
1892 990
            $mapping['association'] = self::REFERENCE_MANY;
1893
        }
1894 1467
        if (isset($mapping['embedded']) && $mapping['type'] === 'one') {
1895 986
            $mapping['association'] = self::EMBED_ONE;
1896
        }
1897 1467
        if (isset($mapping['embedded']) && $mapping['type'] === 'many') {
1898 1027
            $mapping['association'] = self::EMBED_MANY;
1899
        }
1900
1901 1467
        if (isset($mapping['association']) && ! isset($mapping['targetDocument']) && ! isset($mapping['discriminatorField'])) {
1902 144
            $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD;
1903
        }
1904
1905 1467
        if (isset($mapping['version'])) {
1906 108
            $mapping['notSaved'] = true;
1907 108
            $this->setVersionMapping($mapping);
1908
        }
1909 1467
        if (isset($mapping['lock'])) {
1910 22
            $mapping['notSaved'] = true;
1911 22
            $this->setLockMapping($mapping);
1912
        }
1913 1467
        $mapping['isOwningSide']  = true;
1914 1467
        $mapping['isInverseSide'] = false;
1915 1467
        if (isset($mapping['reference'])) {
1916 1113
            if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
1917 104
                $mapping['isOwningSide']  = true;
1918 104
                $mapping['isInverseSide'] = false;
1919
            }
1920 1113
            if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
1921 838
                $mapping['isInverseSide'] = true;
1922 838
                $mapping['isOwningSide']  = false;
1923
            }
1924 1113
            if (isset($mapping['repositoryMethod'])) {
1925 78
                $mapping['isInverseSide'] = true;
1926 78
                $mapping['isOwningSide']  = false;
1927
            }
1928 1113
            if (! isset($mapping['orphanRemoval'])) {
1929 1094
                $mapping['orphanRemoval'] = false;
1930
            }
1931
        }
1932
1933 1467
        if (! empty($mapping['prime']) && ($mapping['association'] !== self::REFERENCE_MANY || ! $mapping['isInverseSide'])) {
1934
            throw MappingException::referencePrimersOnlySupportedForInverseReferenceMany($this->name, $mapping['fieldName']);
1935
        }
1936
1937 1467
        if ($this->isFile && ! $this->isAllowedGridFSField($mapping['name'])) {
1938 1
            throw MappingException::fieldNotAllowedForGridFS($this->name, $mapping['fieldName']);
1939
        }
1940
1941 1466
        $this->applyStorageStrategy($mapping);
1942 1465
        $this->checkDuplicateMapping($mapping);
1943
1944 1465
        $this->fieldMappings[$mapping['fieldName']] = $mapping;
1945 1465
        if (isset($mapping['association'])) {
1946 1268
            $this->associationMappings[$mapping['fieldName']] = $mapping;
1947
        }
1948
1949 1465
        $reflProp = $this->reflClass->getProperty($mapping['fieldName']);
1950 1464
        $reflProp->setAccessible(true);
1951 1464
        $this->reflFields[$mapping['fieldName']] = $reflProp;
1952
1953 1464
        return $mapping;
1954
    }
1955
1956
    /**
1957
     * Determines which fields get serialized.
1958
     *
1959
     * It is only serialized what is necessary for best unserialization performance.
1960
     * That means any metadata properties that are not set or empty or simply have
1961
     * their default value are NOT serialized.
1962
     *
1963
     * Parts that are also NOT serialized because they can not be properly unserialized:
1964
     *      - reflClass (ReflectionClass)
1965
     *      - reflFields (ReflectionProperty array)
1966
     *
1967
     * @return array The names of all the fields that should be serialized.
1968
     */
1969 6
    public function __sleep()
1970
    {
1971
        // This metadata is always serialized/cached.
1972
        $serialized = [
1973 6
            'fieldMappings',
1974
            'associationMappings',
1975
            'identifier',
1976
            'name',
1977
            'db',
1978
            'collection',
1979
            'readPreference',
1980
            'readPreferenceTags',
1981
            'writeConcern',
1982
            'rootDocumentName',
1983
            'generatorType',
1984
            'generatorOptions',
1985
            'idGenerator',
1986
            'indexes',
1987
            'shardKey',
1988
        ];
1989
1990
        // The rest of the metadata is only serialized if necessary.
1991 6
        if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
1992
            $serialized[] = 'changeTrackingPolicy';
1993
        }
1994
1995 6
        if ($this->customRepositoryClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customRepositoryClassName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1996 1
            $serialized[] = 'customRepositoryClassName';
1997
        }
1998
1999 6
        if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE || $this->discriminatorField !== null) {
2000 4
            $serialized[] = 'inheritanceType';
2001 4
            $serialized[] = 'discriminatorField';
2002 4
            $serialized[] = 'discriminatorValue';
2003 4
            $serialized[] = 'discriminatorMap';
2004 4
            $serialized[] = 'defaultDiscriminatorValue';
2005 4
            $serialized[] = 'parentClasses';
2006 4
            $serialized[] = 'subClasses';
2007
        }
2008
2009 6
        if ($this->isMappedSuperclass) {
2010 1
            $serialized[] = 'isMappedSuperclass';
2011
        }
2012
2013 6
        if ($this->isEmbeddedDocument) {
2014 1
            $serialized[] = 'isEmbeddedDocument';
2015
        }
2016
2017 6
        if ($this->isQueryResultDocument) {
2018
            $serialized[] = 'isQueryResultDocument';
2019
        }
2020
2021 6
        if ($this->isFile) {
2022
            $serialized[] = 'isFile';
2023
            $serialized[] = 'bucketName';
2024
            $serialized[] = 'chunkSizeBytes';
2025
        }
2026
2027 6
        if ($this->isVersioned) {
2028
            $serialized[] = 'isVersioned';
2029
            $serialized[] = 'versionField';
2030
        }
2031
2032 6
        if ($this->lifecycleCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lifecycleCallbacks of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2033
            $serialized[] = 'lifecycleCallbacks';
2034
        }
2035
2036 6
        if ($this->collectionCapped) {
2037 1
            $serialized[] = 'collectionCapped';
2038 1
            $serialized[] = 'collectionSize';
2039 1
            $serialized[] = 'collectionMax';
2040
        }
2041
2042 6
        if ($this->isReadOnly) {
2043
            $serialized[] = 'isReadOnly';
2044
        }
2045
2046 6
        return $serialized;
2047
    }
2048
2049
    /**
2050
     * Restores some state that can not be serialized/unserialized.
2051
     */
2052 6
    public function __wakeup()
2053
    {
2054
        // Restore ReflectionClass and properties
2055 6
        $this->reflClass    = new ReflectionClass($this->name);
2056 6
        $this->instantiator = $this->instantiator ?: new Instantiator();
2057
2058 6
        foreach ($this->fieldMappings as $field => $mapping) {
2059 3
            if (isset($mapping['declared'])) {
2060 1
                $reflField = new ReflectionProperty($mapping['declared'], $field);
2061
            } else {
2062 3
                $reflField = $this->reflClass->getProperty($field);
2063
            }
2064 3
            $reflField->setAccessible(true);
2065 3
            $this->reflFields[$field] = $reflField;
2066
        }
2067 6
    }
2068
2069
    /**
2070
     * Creates a new instance of the mapped class, without invoking the constructor.
2071
     */
2072 367
    public function newInstance() : object
2073
    {
2074 367
        return $this->instantiator->instantiate($this->name);
2075
    }
2076
2077 81
    private function isAllowedGridFSField(string $name) : bool
2078
    {
2079 81
        return in_array($name, self::ALLOWED_GRIDFS_FIELDS, true);
2080
    }
2081
2082 1465
    private function checkDuplicateMapping(array $mapping) : void
2083
    {
2084 1465
        if ($mapping['notSaved'] ?? false) {
2085 867
            return;
2086
        }
2087
2088 1465
        foreach ($this->fieldMappings as $fieldName => $otherMapping) {
2089
            // Ignore fields with the same name - we can safely override their mapping
2090 1419
            if ($mapping['fieldName'] === $fieldName) {
2091 72
                continue;
2092
            }
2093
2094
            // Ignore fields with a different name in the database
2095 1415
            if ($mapping['name'] !== $otherMapping['name']) {
2096 1415
                continue;
2097
            }
2098
2099
            // If the other field is not saved, ignore it as well
2100 2
            if ($otherMapping['notSaved'] ?? false) {
2101
                continue;
2102
            }
2103
2104 2
            throw MappingException::duplicateDatabaseFieldName($this->getName(), $mapping['fieldName'], $mapping['name'], $fieldName);
2105
        }
2106 1465
    }
2107
}
2108