Completed
Push — master ( bb3219...cc367c )
by Andreas
15:44
created

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php (1 issue)

Labels
Severity

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 1552
    public function __construct(string $documentName)
510
    {
511 1552
        $this->name             = $documentName;
512 1552
        $this->rootDocumentName = $documentName;
513 1552
        $this->reflClass        = new ReflectionClass($documentName);
514 1552
        $this->setCollection($this->reflClass->getShortName());
515 1552
        $this->instantiator = new Instantiator();
516 1552
    }
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 122
    public static function getReferenceId($reference, string $storeAs)
528
    {
529 122
        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 190
    private static function getReferencePrefix(string $storeAs) : string
536
    {
537 190
        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 190
        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 1436
    public function getReflectionClass() : ReflectionClass
564
    {
565 1436
        if (! $this->reflClass) {
566
            $this->reflClass = new ReflectionClass($this->name);
567
        }
568
569 1436
        return $this->reflClass;
570
    }
571
572
    /**
573
     * {@inheritDoc}
574
     */
575 317
    public function isIdentifier($fieldName) : bool
576
    {
577 317
        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 105
    public function getIdentifierFieldNames() : array
607
    {
608 105
        return [$this->identifier];
609
    }
610
611
    /**
612
     * {@inheritDoc}
613
     */
614 924
    public function hasField($fieldName) : bool
615
    {
616 924
        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 1432
    public function isInheritedField(string $fieldName) : bool
631
    {
632 1432
        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 638
    public function invokeLifecycleCallbacks(string $event, object $document, ?array $arguments = null) : void
655
    {
656 638
        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 637
        if (empty($this->lifecycleCallbacks[$event])) {
661 622
            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 1153
    public function isSharded() : bool
948
    {
949 1153
        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 585
    public function hasWriteConcern() : bool
986
    {
987 585
        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 65
    public function isChangeTrackingDeferredExplicit() : bool
1002
    {
1003 65
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
1004
    }
1005
1006
    /**
1007
     * Whether the change tracking policy of this class is "deferred implicit".
1008
     */
1009 608
    public function isChangeTrackingDeferredImplicit() : bool
1010
    {
1011 608
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
1012
    }
1013
1014
    /**
1015
     * Whether the change tracking policy of this class is "notify".
1016
     */
1017 341
    public function isChangeTrackingNotify() : bool
1018
    {
1019 341
        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 104
    public function getReflectionProperty(string $name) : ReflectionProperty
1034
    {
1035 104
        return $this->reflFields[$name];
1036
    }
1037
1038
    /**
1039
     * {@inheritDoc}
1040
     */
1041 1440
    public function getName() : string
1042
    {
1043 1440
        return $this->name;
1044
    }
1045
1046
    /**
1047
     * Returns the database this Document is mapped to.
1048
     */
1049 1359
    public function getDatabase() : ?string
1050
    {
1051 1359
        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 1351
    public function getCollection() : string
1066
    {
1067 1351
        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 1552
    public function setCollection($name) : void
1078
    {
1079 1552
        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 1552
            $this->collection = $name;
1089
        }
1090 1552
    }
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 1459
    private function applyStorageStrategy(array &$mapping) : void
1175
    {
1176 1459
        if (! isset($mapping['type']) || isset($mapping['id'])) {
1177 1439
            return;
1178
        }
1179
1180
        switch (true) {
1181 1424
            case $mapping['type'] === 'int':
1182 1423
            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 1423
            case $mapping['type'] === 'many':
1188 1119
                $defaultStrategy   = CollectionHelper::DEFAULT_STRATEGY;
1189
                $allowedStrategies = [
1190 1119
                    self::STORAGE_STRATEGY_PUSH_ALL,
1191 1119
                    self::STORAGE_STRATEGY_ADD_TO_SET,
1192 1119
                    self::STORAGE_STRATEGY_SET,
1193 1119
                    self::STORAGE_STRATEGY_SET_ARRAY,
1194 1119
                    self::STORAGE_STRATEGY_ATOMIC_SET,
1195 1119
                    self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY,
1196
                ];
1197 1119
                break;
1198
1199
            default:
1200 1411
                $defaultStrategy   = self::STORAGE_STRATEGY_SET;
1201 1411
                $allowedStrategies = [self::STORAGE_STRATEGY_SET];
1202
        }
1203
1204 1424
        if (! isset($mapping['strategy'])) {
1205 1416
            $mapping['strategy'] = $defaultStrategy;
1206
        }
1207
1208 1424
        if (! in_array($mapping['strategy'], $allowedStrategies)) {
1209
            throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']);
1210
        }
1211
1212 1424
        if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide']
1213 1424
            && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])) {
1214 1
            throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']);
1215
        }
1216 1423
    }
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 1374
    public function setIdGenerator(AbstractIdGenerator $generator) : void
1378
    {
1379 1374
        $this->idGenerator = $generator;
1380 1374
    }
1381
1382
    /**
1383
     * Casts the identifier to its portable PHP type.
1384
     *
1385
     * @param mixed $id
1386
     *
1387
     * @return mixed $id
1388
     */
1389 637
    public function getPHPIdentifierValue($id)
1390
    {
1391 637
        $idType = $this->fieldMappings[$this->identifier]['type'];
1392 637
        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 701
    public function getDatabaseIdentifierValue($id)
1403
    {
1404 701
        $idType = $this->fieldMappings[$this->identifier]['type'];
1405 701
        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 566
    public function setIdentifierValue(object $document, $id) : void
1416
    {
1417 566
        $id = $this->getPHPIdentifierValue($id);
1418 566
        $this->reflFields[$this->identifier]->setValue($document, $id);
1419 566
    }
1420
1421
    /**
1422
     * Gets the document identifier as a PHP type.
1423
     *
1424
     * @return mixed $id
1425
     */
1426 644
    public function getIdentifierValue(object $document)
1427
    {
1428 644
        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 596
    public function getEmbeddedFieldsMappings() : array
1500
    {
1501 596
        return array_filter(
1502 596
            $this->associationMappings,
1503
            static function ($assoc) {
1504 458
                return ! empty($assoc['embedded']);
1505 596
            }
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 525
    public function hasDiscriminator() : bool
1542
    {
1543 525
        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 605
    public function isInheritanceTypeNone() : bool
1563
    {
1564 605
        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 1429
    public function setParentClasses(array $classNames) : void
1603
    {
1604 1429
        $this->parentClasses = $classNames;
1605
1606 1429
        if (count($classNames) <= 0) {
1607 1428
            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 1475
    public function mapField(array $mapping) : array
1794
    {
1795 1475
        if (! isset($mapping['fieldName']) && isset($mapping['name'])) {
1796 5
            $mapping['fieldName'] = $mapping['name'];
1797
        }
1798 1475
        if (! isset($mapping['fieldName'])) {
1799
            throw MappingException::missingFieldName($this->name);
1800
        }
1801 1475
        if (! isset($mapping['name'])) {
1802 1474
            $mapping['name'] = $mapping['fieldName'];
1803
        }
1804 1475
        if ($this->identifier === $mapping['name'] && empty($mapping['id'])) {
1805 1
            throw MappingException::mustNotChangeIdentifierFieldsType($this->name, $mapping['name']);
1806
        }
1807 1474
        if ($this->discriminatorField !== null && $this->discriminatorField === $mapping['name']) {
1808 1
            throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField);
1809
        }
1810 1473
        if (isset($mapping['collectionClass'])) {
1811 72
            $mapping['collectionClass'] = ltrim($mapping['collectionClass'], '\\');
1812
        }
1813 1473
        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 1472
        if (isset($mapping['cascade']) && isset($mapping['embedded'])) {
1821 1
            throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']);
1822
        }
1823
1824 1471
        $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : [];
1825
1826 1471
        if (in_array('all', $cascades) || isset($mapping['embedded'])) {
1827 1163
            $cascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
1828
        }
1829
1830 1471
        if (isset($mapping['embedded'])) {
1831 1124
            unset($mapping['cascade']);
1832 1466
        } elseif (isset($mapping['cascade'])) {
1833 938
            $mapping['cascade'] = $cascades;
1834
        }
1835
1836 1471
        $mapping['isCascadeRemove']  = in_array('remove', $cascades);
1837 1471
        $mapping['isCascadePersist'] = in_array('persist', $cascades);
1838 1471
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades);
1839 1471
        $mapping['isCascadeMerge']   = in_array('merge', $cascades);
1840 1471
        $mapping['isCascadeDetach']  = in_array('detach', $cascades);
1841
1842 1471
        if (isset($mapping['id']) && $mapping['id'] === true) {
1843 1437
            $mapping['name']  = '_id';
1844 1437
            $this->identifier = $mapping['fieldName'];
1845 1437
            if (isset($mapping['strategy'])) {
1846 1431
                $this->generatorType = constant(self::class . '::GENERATOR_TYPE_' . strtoupper($mapping['strategy']));
1847
            }
1848 1437
            $this->generatorOptions = $mapping['options'] ?? [];
1849 1437
            switch ($this->generatorType) {
1850 1437
                case self::GENERATOR_TYPE_AUTO:
1851 1365
                    $mapping['type'] = 'id';
1852 1365
                    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 1437
            unset($this->generatorOptions['type']);
1861
        }
1862
1863 1471
        if (! isset($mapping['nullable'])) {
1864 40
            $mapping['nullable'] = false;
1865
        }
1866
1867 1471
        if (isset($mapping['reference'])
1868 1471
            && isset($mapping['storeAs'])
1869 1471
            && $mapping['storeAs'] === self::REFERENCE_STORE_AS_ID
1870 1471
            && ! isset($mapping['targetDocument'])
1871
        ) {
1872 3
            throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']);
1873
        }
1874
1875 1468
        if (isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) &&
1876 1468
                (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))) {
1877 4
            throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']);
1878
        }
1879
1880 1464
        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 1463
        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 1460
        if (isset($mapping['reference']) && $mapping['type'] === 'one') {
1889 1046
            $mapping['association'] = self::REFERENCE_ONE;
1890
        }
1891 1460
        if (isset($mapping['reference']) && $mapping['type'] === 'many') {
1892 989
            $mapping['association'] = self::REFERENCE_MANY;
1893
        }
1894 1460
        if (isset($mapping['embedded']) && $mapping['type'] === 'one') {
1895 986
            $mapping['association'] = self::EMBED_ONE;
1896
        }
1897 1460
        if (isset($mapping['embedded']) && $mapping['type'] === 'many') {
1898 1020
            $mapping['association'] = self::EMBED_MANY;
1899
        }
1900
1901 1460
        if (isset($mapping['association']) && ! isset($mapping['targetDocument']) && ! isset($mapping['discriminatorField'])) {
1902 144
            $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD;
1903
        }
1904
1905 1460
        if (isset($mapping['version'])) {
1906 108
            $mapping['notSaved'] = true;
1907 108
            $this->setVersionMapping($mapping);
1908
        }
1909 1460
        if (isset($mapping['lock'])) {
1910 22
            $mapping['notSaved'] = true;
1911 22
            $this->setLockMapping($mapping);
1912
        }
1913 1460
        $mapping['isOwningSide']  = true;
1914 1460
        $mapping['isInverseSide'] = false;
1915 1460
        if (isset($mapping['reference'])) {
1916 1112
            if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
1917 104
                $mapping['isOwningSide']  = true;
1918 104
                $mapping['isInverseSide'] = false;
1919
            }
1920 1112
            if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
1921 838
                $mapping['isInverseSide'] = true;
1922 838
                $mapping['isOwningSide']  = false;
1923
            }
1924 1112
            if (isset($mapping['repositoryMethod'])) {
1925 78
                $mapping['isInverseSide'] = true;
1926 78
                $mapping['isOwningSide']  = false;
1927
            }
1928 1112
            if (! isset($mapping['orphanRemoval'])) {
1929 1093
                $mapping['orphanRemoval'] = false;
1930
            }
1931
        }
1932
1933 1460
        if (! empty($mapping['prime']) && ($mapping['association'] !== self::REFERENCE_MANY || ! $mapping['isInverseSide'])) {
1934
            throw MappingException::referencePrimersOnlySupportedForInverseReferenceMany($this->name, $mapping['fieldName']);
1935
        }
1936
1937 1460
        if ($this->isFile && ! $this->isAllowedGridFSField($mapping['name'])) {
1938 1
            throw MappingException::fieldNotAllowedForGridFS($this->name, $mapping['fieldName']);
1939
        }
1940
1941 1459
        $this->applyStorageStrategy($mapping);
1942 1458
        $this->checkDuplicateMapping($mapping);
1943
1944 1458
        $this->fieldMappings[$mapping['fieldName']] = $mapping;
1945 1458
        if (isset($mapping['association'])) {
1946 1261
            $this->associationMappings[$mapping['fieldName']] = $mapping;
1947
        }
1948
1949 1458
        $reflProp = $this->reflClass->getProperty($mapping['fieldName']);
1950 1457
        $reflProp->setAccessible(true);
1951 1457
        $this->reflFields[$mapping['fieldName']] = $reflProp;
1952
1953 1457
        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) {
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) {
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 379
    public function newInstance() : object
2073
    {
2074 379
        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 1458
    private function checkDuplicateMapping(array $mapping) : void
2083
    {
2084 1458
        if ($mapping['notSaved'] ?? false) {
2085 867
            return;
2086
        }
2087
2088 1458
        foreach ($this->fieldMappings as $fieldName => $otherMapping) {
2089
            // Ignore fields with the same name - we can safely override their mapping
2090 1412
            if ($mapping['fieldName'] === $fieldName) {
2091 72
                continue;
2092
            }
2093
2094
            // Ignore fields with a different name in the database
2095 1408
            if ($mapping['name'] !== $otherMapping['name']) {
2096 1408
                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);
0 ignored issues
show
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
2105
        }
2106 1458
    }
2107
}
2108