Completed
Pull Request — master (#1790)
by Andreas
14:35
created

ClassMetadata::addLifecycleCallback()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 4
nc 2
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Mapping;
6
7
use Doctrine\Common\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
8
use Doctrine\Instantiator\Instantiator;
9
use Doctrine\Instantiator\InstantiatorInterface;
10
use Doctrine\ODM\MongoDB\Id\AbstractIdGenerator;
11
use Doctrine\ODM\MongoDB\LockException;
12
use Doctrine\ODM\MongoDB\Proxy\Proxy;
13
use Doctrine\ODM\MongoDB\Types\Type;
14
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
15
use InvalidArgumentException;
16
use MongoDB\BSON\ObjectId;
17
use function array_filter;
18
use function array_key_exists;
19
use function array_keys;
20
use function array_map;
21
use function array_pop;
22
use function call_user_func_array;
23
use function class_exists;
24
use function constant;
25
use function count;
26
use function get_class;
27
use function in_array;
28
use function is_array;
29
use function is_string;
30
use function is_subclass_of;
31
use function ltrim;
32
use function sprintf;
33
use function strlen;
34
use function strpos;
35
use function strtolower;
36
use function strtoupper;
37
38
/**
39
 * A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata
40
 * of a document and it's references.
41
 *
42
 * Once populated, ClassMetadata instances are usually cached in a serialized form.
43
 *
44
 * <b>IMPORTANT NOTE:</b>
45
 *
46
 * The fields of this class are only public for 2 reasons:
47
 * 1) To allow fast READ access.
48
 * 2) To drastically reduce the size of a serialized instance (private/protected members
49
 *    get the whole class name, namespace inclusive, prepended to every property in
50
 *    the serialized representation).
51
 *
52
 */
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
    /**
177
     * READ-ONLY: The name of the mongo database the document is mapped to.
178
     * @var string
179
     */
180
    public $db;
181
182
    /**
183
     * READ-ONLY: The name of the mongo collection the document is mapped to.
184
     * @var string
185
     */
186
    public $collection;
187
188
    /**
189
     * READ-ONLY: If the collection should be a fixed size.
190
     * @var bool
191
     */
192
    public $collectionCapped;
193
194
    /**
195
     * READ-ONLY: If the collection is fixed size, its size in bytes.
196
     * @var int|null
197
     */
198
    public $collectionSize;
199
200
    /**
201
     * READ-ONLY: If the collection is fixed size, the maximum number of elements to store in the collection.
202
     * @var int|null
203
     */
204
    public $collectionMax;
205
206
    /**
207
     * READ-ONLY Describes how MongoDB clients route read operations to the members of a replica set.
208
     * @var string|int|null
209
     */
210
    public $readPreference;
211
212
    /**
213
     * READ-ONLY Associated with readPreference Allows to specify criteria so that your application can target read
214
     * operations to specific members, based on custom parameters.
215
     * @var string[][]|null
216
     */
217
    public $readPreferenceTags;
218
219
    /**
220
     * READ-ONLY: Describes the level of acknowledgement requested from MongoDB for write operations.
221
     * @var string|int|null
222
     */
223
    public $writeConcern;
224
225
    /**
226
     * READ-ONLY: The field name of the document identifier.
227
     * @var string|null
228
     */
229
    public $identifier;
230
231
    /**
232
     * READ-ONLY: The array of indexes for the document collection.
233
     * @var array
234
     */
235
    public $indexes = [];
236
237
    /**
238
     * READ-ONLY: Keys and options describing shard key. Only for sharded collections.
239
     * @var string|null
240
     */
241
    public $shardKey;
242
243
    /**
244
     * READ-ONLY: The name of the document class.
245
     * @var string
246
     */
247
    public $name;
248
249
    /**
250
     * READ-ONLY: The namespace the document class is contained in.
251
     *
252
     * @var string
253
     * @todo Not really needed. Usage could be localized.
254
     */
255
    public $namespace;
256
257
    /**
258
     * READ-ONLY: The name of the document class that is at the root of the mapped document inheritance
259
     * hierarchy. If the document is not part of a mapped inheritance hierarchy this is the same
260
     * as {@link $documentName}.
261
     *
262
     * @var string
263
     */
264
    public $rootDocumentName;
265
266
    /**
267
     * The name of the custom repository class used for the document class.
268
     * (Optional).
269
     *
270
     * @var string
271
     */
272
    public $customRepositoryClassName;
273
274
    /**
275
     * READ-ONLY: The names of the parent classes (ancestors).
276
     *
277
     * @var array
278
     */
279
    public $parentClasses = [];
280
281
    /**
282
     * READ-ONLY: The names of all subclasses (descendants).
283
     *
284
     * @var array
285
     */
286
    public $subClasses = [];
287
288
    /**
289
     * The ReflectionProperty instances of the mapped class.
290
     *
291
     * @var \ReflectionProperty[]
292
     */
293
    public $reflFields = [];
294
295
    /**
296
     * READ-ONLY: The inheritance mapping type used by the class.
297
     *
298
     * @var int
299
     */
300
    public $inheritanceType = self::INHERITANCE_TYPE_NONE;
301
302
    /**
303
     * READ-ONLY: The Id generator type used by the class.
304
     *
305
     * @var string
306
     */
307
    public $generatorType = self::GENERATOR_TYPE_AUTO;
308
309
    /**
310
     * READ-ONLY: The Id generator options.
311
     *
312
     * @var array
313
     */
314
    public $generatorOptions = [];
315
316
    /**
317
     * READ-ONLY: The ID generator used for generating IDs for this class.
318
     *
319
     * @var AbstractIdGenerator
320
     */
321
    public $idGenerator;
322
323
    /**
324
     * READ-ONLY: The field mappings of the class.
325
     * Keys are field names and values are mapping definitions.
326
     *
327
     * The mapping definition array has the following values:
328
     *
329
     * - <b>fieldName</b> (string)
330
     * The name of the field in the Document.
331
     *
332
     * - <b>id</b> (boolean, optional)
333
     * Marks the field as the primary key of the document. Multiple fields of an
334
     * document can have the id attribute, forming a composite key.
335
     *
336
     * @var array
337
     */
338
    public $fieldMappings = [];
339
340
    /**
341
     * READ-ONLY: The association mappings of the class.
342
     * Keys are field names and values are mapping definitions.
343
     *
344
     * @var array
345
     */
346
    public $associationMappings = [];
347
348
    /**
349
     * READ-ONLY: Array of fields to also load with a given method.
350
     *
351
     * @var array
352
     */
353
    public $alsoLoadMethods = [];
354
355
    /**
356
     * READ-ONLY: The registered lifecycle callbacks for documents of this class.
357
     *
358
     * @var array
359
     */
360
    public $lifecycleCallbacks = [];
361
362
    /**
363
     * READ-ONLY: The discriminator value of this class.
364
     *
365
     * <b>This does only apply to the JOINED and SINGLE_COLLECTION inheritance mapping strategies
366
     * where a discriminator field is used.</b>
367
     *
368
     * @var mixed
369
     * @see discriminatorField
370
     */
371
    public $discriminatorValue;
372
373
    /**
374
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
375
     *
376
     * <b>This does only apply to the SINGLE_COLLECTION inheritance mapping strategy
377
     * where a discriminator field is used.</b>
378
     *
379
     * @var mixed
380
     * @see discriminatorField
381
     */
382
    public $discriminatorMap = [];
383
384
    /**
385
     * READ-ONLY: The definition of the discriminator field used in SINGLE_COLLECTION
386
     * inheritance mapping.
387
     *
388
     * @var string
389
     */
390
    public $discriminatorField;
391
392
    /**
393
     * READ-ONLY: The default value for discriminatorField in case it's not set in the document
394
     *
395
     * @var string
396
     * @see discriminatorField
397
     */
398
    public $defaultDiscriminatorValue;
399
400
    /**
401
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
402
     *
403
     * @var bool
404
     */
405
    public $isMappedSuperclass = false;
406
407
    /**
408
     * READ-ONLY: Whether this class describes the mapping of a embedded document.
409
     *
410
     * @var bool
411
     */
412
    public $isEmbeddedDocument = false;
413
414
    /**
415
     * READ-ONLY: Whether this class describes the mapping of an aggregation result document.
416
     *
417
     * @var bool
418
     */
419
    public $isQueryResultDocument = false;
420
421
    /**
422
     * READ-ONLY: Whether this class describes the mapping of a gridFS file
423
     *
424
     * @var bool
425
     */
426
    public $isFile = false;
427
428
    /**
429
     * READ-ONLY: The policy used for change-tracking on entities of this class.
430
     *
431
     * @var int
432
     */
433
    public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
434
435
    /**
436
     * READ-ONLY: A flag for whether or not instances of this class are to be versioned
437
     * with optimistic locking.
438
     *
439
     * @var bool $isVersioned
440
     */
441
    public $isVersioned;
442
443
    /**
444
     * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
445
     *
446
     * @var mixed $versionField
447
     */
448
    public $versionField;
449
450
    /**
451
     * READ-ONLY: A flag for whether or not instances of this class are to allow pessimistic
452
     * locking.
453
     *
454
     * @var bool $isLockable
455
     */
456
    public $isLockable;
457
458
    /**
459
     * READ-ONLY: The name of the field which is used for locking a document.
460
     *
461
     * @var mixed $lockField
462
     */
463
    public $lockField;
464
465
    /**
466
     * The ReflectionClass instance of the mapped class.
467
     *
468
     * @var \ReflectionClass
469
     */
470
    public $reflClass;
471
472
    /**
473
     * READ_ONLY: A flag for whether or not this document is read-only.
474
     *
475
     * @var bool
476
     */
477
    public $isReadOnly;
478
479
    /** @var InstantiatorInterface|null */
480
    private $instantiator;
481
482
    /**
483
     * Initializes a new ClassMetadata instance that will hold the object-document mapping
484
     * metadata of the class with the given name.
485
     *
486
     * @param string $documentName The name of the document class the new instance is used for.
487
     */
488 1492
    public function __construct($documentName)
489
    {
490 1492
        $this->name = $documentName;
491 1492
        $this->rootDocumentName = $documentName;
492 1492
        $this->reflClass = new \ReflectionClass($documentName);
493 1492
        $this->namespace = $this->reflClass->getNamespaceName();
494 1492
        $this->setCollection($this->reflClass->getShortName());
495 1492
        $this->instantiator = new Instantiator();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Doctrine\Instantiator\Instantiator() of type object<Doctrine\Instantiator\Instantiator> is incompatible with the declared type object<Doctrine\Instanti...antiatorInterface>|null of property $instantiator.

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

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

Loading history...
496 1492
    }
497
498
    /**
499
     * Helper method to get reference id of ref* type references
500
     * @param mixed  $reference
501
     * @param string $storeAs
502
     * @return mixed
503
     * @internal
504
     */
505 122
    public static function getReferenceId($reference, $storeAs)
506
    {
507 122
        return $storeAs === self::REFERENCE_STORE_AS_ID ? $reference : $reference[self::getReferencePrefix($storeAs) . 'id'];
508
    }
509
510
    /**
511
     * Returns the reference prefix used for a reference
512
     * @param string $storeAs
513
     * @return string
514
     */
515 187
    private static function getReferencePrefix($storeAs)
516
    {
517 187
        if (! in_array($storeAs, [self::REFERENCE_STORE_AS_REF, self::REFERENCE_STORE_AS_DB_REF, self::REFERENCE_STORE_AS_DB_REF_WITH_DB])) {
518
            throw new \LogicException('Can only get a reference prefix for DBRef and reference arrays');
519
        }
520
521 187
        return $storeAs === self::REFERENCE_STORE_AS_REF ? '' : '$';
522
    }
523
524
    /**
525
     * Returns a fully qualified field name for a given reference
526
     * @param string $storeAs
527
     * @param string $pathPrefix The field path prefix
528
     * @return string
529
     * @internal
530
     */
531 134
    public static function getReferenceFieldName($storeAs, $pathPrefix = '')
532
    {
533 134
        if ($storeAs === self::REFERENCE_STORE_AS_ID) {
534 94
            return $pathPrefix;
535
        }
536
537 122
        return ($pathPrefix ? $pathPrefix . '.' : '') . static::getReferencePrefix($storeAs) . 'id';
0 ignored issues
show
Bug introduced by
Since getReferencePrefix() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of getReferencePrefix() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
538
    }
539
540
    /**
541
     * {@inheritDoc}
542
     */
543 1378
    public function getReflectionClass()
544
    {
545 1378
        if (! $this->reflClass) {
546
            $this->reflClass = new \ReflectionClass($this->name);
547
        }
548
549 1378
        return $this->reflClass;
550
    }
551
552
    /**
553
     * {@inheritDoc}
554
     */
555 325
    public function isIdentifier($fieldName)
556
    {
557 325
        return $this->identifier === $fieldName;
558
    }
559
560
    /**
561
     * INTERNAL:
562
     * Sets the mapped identifier field of this class.
563
     *
564
     * @param string $identifier
565
     */
566 892
    public function setIdentifier($identifier)
567
    {
568 892
        $this->identifier = $identifier;
569 892
    }
570
571
    /**
572
     * {@inheritDoc}
573
     *
574
     * Since MongoDB only allows exactly one identifier field
575
     * this will always return an array with only one value
576
     */
577 39
    public function getIdentifier()
578
    {
579 39
        return [$this->identifier];
580
    }
581
582
    /**
583
     * {@inheritDoc}
584
     *
585
     * Since MongoDB only allows exactly one identifier field
586
     * this will always return an array with only one value
587
     */
588 99
    public function getIdentifierFieldNames()
589
    {
590 99
        return [$this->identifier];
591
    }
592
593
    /**
594
     * {@inheritDoc}
595
     */
596 893
    public function hasField($fieldName)
597
    {
598 893
        return isset($this->fieldMappings[$fieldName]);
599
    }
600
601
    /**
602
     * Sets the inheritance type used by the class and it's subclasses.
603
     *
604
     * @param int $type
605
     */
606 908
    public function setInheritanceType($type)
607
    {
608 908
        $this->inheritanceType = $type;
609 908
    }
610
611
    /**
612
     * Checks whether a mapped field is inherited from an entity superclass.
613
     *
614
     * @param  string $fieldName
615
     *
616
     * @return bool TRUE if the field is inherited, FALSE otherwise.
617
     */
618 1374
    public function isInheritedField($fieldName)
619
    {
620 1374
        return isset($this->fieldMappings[$fieldName]['inherited']);
621
    }
622
623
    /**
624
     * Registers a custom repository class for the document class.
625
     *
626
     * @param string $repositoryClassName The class name of the custom repository.
627
     */
628 840
    public function setCustomRepositoryClass($repositoryClassName)
629
    {
630 840
        if ($this->isEmbeddedDocument || $this->isQueryResultDocument) {
631
            return;
632
        }
633
634 840
        if ($repositoryClassName && strpos($repositoryClassName, '\\') === false && strlen($this->namespace)) {
635 3
            $repositoryClassName = $this->namespace . '\\' . $repositoryClassName;
636
        }
637
638 840
        $this->customRepositoryClassName = $repositoryClassName;
639 840
    }
640
641
    /**
642
     * Dispatches the lifecycle event of the given document by invoking all
643
     * registered callbacks.
644
     *
645
     * @param string $event     Lifecycle event
646
     * @param object $document  Document on which the event occurred
647
     * @param array  $arguments Arguments to pass to all callbacks
648
     * @throws \InvalidArgumentException If document class is not this class or
649
     *                                   a Proxy of this class.
650
     */
651 602
    public function invokeLifecycleCallbacks($event, $document, ?array $arguments = null)
652
    {
653 602
        if (! $document instanceof $this->name) {
654 1
            throw new \InvalidArgumentException(sprintf('Expected document class "%s"; found: "%s"', $this->name, get_class($document)));
655
        }
656
657 601
        if (empty($this->lifecycleCallbacks[$event])) {
658 586
            return;
659
        }
660
661 178
        foreach ($this->lifecycleCallbacks[$event] as $callback) {
662 178
            if ($arguments !== null) {
663 177
                call_user_func_array([$document, $callback], $arguments);
664
            } else {
665 178
                $document->$callback();
666
            }
667
        }
668 178
    }
669
670
    /**
671
     * Checks whether the class has callbacks registered for a lifecycle event.
672
     *
673
     * @param string $event Lifecycle event
674
     *
675
     * @return bool
676
     */
677
    public function hasLifecycleCallbacks($event)
678
    {
679
        return ! empty($this->lifecycleCallbacks[$event]);
680
    }
681
682
    /**
683
     * Gets the registered lifecycle callbacks for an event.
684
     *
685
     * @param string $event
686
     * @return array
687
     */
688
    public function getLifecycleCallbacks($event)
689
    {
690
        return $this->lifecycleCallbacks[$event] ?? [];
691
    }
692
693
    /**
694
     * Adds a lifecycle callback for documents of this class.
695
     *
696
     * If the callback is already registered, this is a NOOP.
697
     *
698
     * @param string $callback
699
     * @param string $event
700
     */
701 807
    public function addLifecycleCallback($callback, $event)
702
    {
703 807
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
704 1
            return;
705
        }
706
707 807
        $this->lifecycleCallbacks[$event][] = $callback;
708 807
    }
709
710
    /**
711
     * Sets the lifecycle callbacks for documents of this class.
712
     *
713
     * Any previously registered callbacks are overwritten.
714
     *
715
     * @param array $callbacks
716
     */
717 891
    public function setLifecycleCallbacks(array $callbacks)
718
    {
719 891
        $this->lifecycleCallbacks = $callbacks;
720 891
    }
721
722
    /**
723
     * Registers a method for loading document data before field hydration.
724
     *
725
     * Note: A method may be registered multiple times for different fields.
726
     * it will be invoked only once for the first field found.
727
     *
728
     * @param string       $method Method name
729
     * @param array|string $fields Database field name(s)
730
     */
731 14
    public function registerAlsoLoadMethod($method, $fields)
732
    {
733 14
        $this->alsoLoadMethods[$method] = is_array($fields) ? $fields : [$fields];
734 14
    }
735
736
    /**
737
     * Sets the AlsoLoad methods for documents of this class.
738
     *
739
     * Any previously registered methods are overwritten.
740
     *
741
     * @param array $methods
742
     */
743 891
    public function setAlsoLoadMethods(array $methods)
744
    {
745 891
        $this->alsoLoadMethods = $methods;
746 891
    }
747
748
    /**
749
     * Sets the discriminator field.
750
     *
751
     * The field name is the the unmapped database field. Discriminator values
752
     * are only used to discern the hydration class and are not mapped to class
753
     * properties.
754
     *
755
     * @param string $discriminatorField
756
     *
757
     * @throws MappingException If the discriminator field conflicts with the
758
     *                          "name" attribute of a mapped field.
759
     */
760 917
    public function setDiscriminatorField($discriminatorField)
761
    {
762 917
        if ($discriminatorField === null) {
763 849
            $this->discriminatorField = null;
764
765 849
            return;
766
        }
767
768 120
        if ($this->isFile) {
769
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
770
        }
771
772
        // Handle array argument with name/fieldName keys for BC
773 120
        if (is_array($discriminatorField)) {
774
            if (isset($discriminatorField['name'])) {
775
                $discriminatorField = $discriminatorField['name'];
776
            } elseif (isset($discriminatorField['fieldName'])) {
777
                $discriminatorField = $discriminatorField['fieldName'];
778
            }
779
        }
780
781 120
        foreach ($this->fieldMappings as $fieldMapping) {
782 4
            if ($discriminatorField === $fieldMapping['name']) {
783 4
                throw MappingException::discriminatorFieldConflict($this->name, $discriminatorField);
784
            }
785
        }
786
787 119
        $this->discriminatorField = $discriminatorField;
788 119
    }
789
790
    /**
791
     * Sets the discriminator values used by this class.
792
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
793
     *
794
     * @param array $map
795
     *
796
     * @throws MappingException
797
     */
798 910
    public function setDiscriminatorMap(array $map)
799
    {
800 910
        if ($this->isFile) {
801
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
802
        }
803
804 910
        foreach ($map as $value => $className) {
805 115
            if (strpos($className, '\\') === false && strlen($this->namespace)) {
806 85
                $className = $this->namespace . '\\' . $className;
807
            }
808 115
            $this->discriminatorMap[$value] = $className;
809 115
            if ($this->name === $className) {
810 107
                $this->discriminatorValue = $value;
811
            } else {
812 114
                if (! class_exists($className)) {
813
                    throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
814
                }
815 114
                if (is_subclass_of($className, $this->name)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $this->name can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
816 115
                    $this->subClasses[] = $className;
817
                }
818
            }
819
        }
820 910
    }
821
822
    /**
823
     * Sets the default discriminator value to be used for this class
824
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies if the document has no discriminator value
825
     *
826
     * @param string $defaultDiscriminatorValue
827
     *
828
     * @throws MappingException
829
     */
830 894
    public function setDefaultDiscriminatorValue($defaultDiscriminatorValue)
831
    {
832 894
        if ($defaultDiscriminatorValue === null) {
833 891
            $this->defaultDiscriminatorValue = null;
834
835 891
            return;
836
        }
837
838 51
        if ($this->isFile) {
839
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
840
        }
841
842 51
        if (! array_key_exists($defaultDiscriminatorValue, $this->discriminatorMap)) {
843
            throw MappingException::invalidDiscriminatorValue($defaultDiscriminatorValue, $this->name);
844
        }
845
846 51
        $this->defaultDiscriminatorValue = $defaultDiscriminatorValue;
847 51
    }
848
849
    /**
850
     * Sets the discriminator value for this class.
851
     * Used for JOINED/SINGLE_TABLE inheritance and multiple document types in a single
852
     * collection.
853
     *
854
     * @param string $value
855
     *
856
     * @throws MappingException
857
     */
858 3
    public function setDiscriminatorValue($value)
859
    {
860 3
        if ($this->isFile) {
861
            throw MappingException::discriminatorNotAllowedForGridFS($this->name);
862
        }
863
864 3
        $this->discriminatorMap[$value] = $this->name;
865 3
        $this->discriminatorValue = $value;
866 3
    }
867
868
    /**
869
     * Add a index for this Document.
870
     *
871
     * @param array $keys    Array of keys for the index.
872
     * @param array $options Array of options for the index.
873
     */
874 186
    public function addIndex($keys, array $options = [])
875
    {
876 186
        $this->indexes[] = [
877
            'keys' => array_map(function ($value) {
878 186
                if ($value === 1 || $value === -1) {
879 48
                    return (int) $value;
880
                }
881 186
                if (is_string($value)) {
882 186
                    $lower = strtolower($value);
883 186
                    if ($lower === 'asc') {
884 179
                        return 1;
885
                    }
886
887 55
                    if ($lower === 'desc') {
888
                        return -1;
889
                    }
890
                }
891 55
                return $value;
892 186
            }, $keys),
893 186
            'options' => $options,
894
        ];
895 186
    }
896
897
    /**
898
     * Returns the array of indexes for this Document.
899
     *
900
     * @return array $indexes The array of indexes.
901
     */
902 24
    public function getIndexes()
903
    {
904 24
        return $this->indexes;
905
    }
906
907
    /**
908
     * Checks whether this document has indexes or not.
909
     *
910
     * @return bool
911
     */
912
    public function hasIndexes()
913
    {
914
        return $this->indexes ? true : false;
915
    }
916
917
    /**
918
     * Set shard key for this Document.
919
     *
920
     * @param array $keys    Array of document keys.
921
     * @param array $options Array of sharding options.
922
     *
923
     * @throws MappingException
924
     */
925 74
    public function setShardKey(array $keys, array $options = [])
926
    {
927 74
        if ($this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION && $this->shardKey !== null) {
928 2
            throw MappingException::shardKeyInSingleCollInheritanceSubclass($this->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
929
        }
930
931 74
        if ($this->isEmbeddedDocument) {
932 2
            throw MappingException::embeddedDocumentCantHaveShardKey($this->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
933
        }
934
935 72
        foreach (array_keys($keys) as $field) {
936 72
            if (! isset($this->fieldMappings[$field])) {
937 65
                continue;
938
            }
939
940 7
            if (in_array($this->fieldMappings[$field]['type'], ['many', 'collection'])) {
941 3
                throw MappingException::noMultiKeyShardKeys($this->getName(), $field);
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
942
            }
943
944 4
            if ($this->fieldMappings[$field]['strategy'] !== static::STORAGE_STRATEGY_SET) {
945 4
                throw MappingException::onlySetStrategyAllowedInShardKey($this->getName(), $field);
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
946
            }
947
        }
948
949 68
        $this->shardKey = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('keys' => \array_m... 'options' => $options) of type array<string,array,{"key...ay","options":"array"}> is incompatible with the declared type string|null of property $shardKey.

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

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

Loading history...
950
            'keys' => array_map(function ($value) {
951 68
                if ($value === 1 || $value === -1) {
952 5
                    return (int) $value;
953
                }
954 68
                if (is_string($value)) {
955 68
                    $lower = strtolower($value);
956 68
                    if ($lower === 'asc') {
957 66
                        return 1;
958
                    }
959
960 50
                    if ($lower === 'desc') {
961
                        return -1;
962
                    }
963
                }
964 50
                return $value;
965 68
            }, $keys),
966 68
            'options' => $options,
967
        ];
968 68
    }
969
970
    /**
971
     * @return array
972
     */
973 17
    public function getShardKey()
974
    {
975 17
        return $this->shardKey;
976
    }
977
978
    /**
979
     * Checks whether this document has shard key or not.
980
     *
981
     * @return bool
982
     */
983 1101
    public function isSharded()
984
    {
985 1101
        return $this->shardKey ? true : false;
986
    }
987
988
    /**
989
     * Sets the read preference used by this class.
990
     *
991
     * @param string     $readPreference
992
     * @param array|null $tags
993
     */
994 891
    public function setReadPreference($readPreference, $tags)
995
    {
996 891
        $this->readPreference = $readPreference;
997 891
        $this->readPreferenceTags = $tags;
998 891
    }
999
1000
    /**
1001
     * Sets the write concern used by this class.
1002
     *
1003
     * @param string $writeConcern
1004
     */
1005 901
    public function setWriteConcern($writeConcern)
1006
    {
1007 901
        $this->writeConcern = $writeConcern;
1008 901
    }
1009
1010
    /**
1011
     * @return string
1012
     */
1013 11
    public function getWriteConcern()
1014
    {
1015 11
        return $this->writeConcern;
1016
    }
1017
1018
    /**
1019
     * Whether there is a write concern configured for this class.
1020
     *
1021
     * @return bool
1022
     */
1023 549
    public function hasWriteConcern()
1024
    {
1025 549
        return $this->writeConcern !== null;
1026
    }
1027
1028
    /**
1029
     * Sets the change tracking policy used by this class.
1030
     *
1031
     * @param int $policy
1032
     */
1033 893
    public function setChangeTrackingPolicy($policy)
1034
    {
1035 893
        $this->changeTrackingPolicy = $policy;
1036 893
    }
1037
1038
    /**
1039
     * Whether the change tracking policy of this class is "deferred explicit".
1040
     *
1041
     * @return bool
1042
     */
1043 64
    public function isChangeTrackingDeferredExplicit()
1044
    {
1045 64
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
1046
    }
1047
1048
    /**
1049
     * Whether the change tracking policy of this class is "deferred implicit".
1050
     *
1051
     * @return bool
1052
     */
1053 571
    public function isChangeTrackingDeferredImplicit()
1054
    {
1055 571
        return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
1056
    }
1057
1058
    /**
1059
     * Whether the change tracking policy of this class is "notify".
1060
     *
1061
     * @return bool
1062
     */
1063 311
    public function isChangeTrackingNotify()
1064
    {
1065 311
        return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
1066
    }
1067
1068
    /**
1069
     * Gets the ReflectionProperties of the mapped class.
1070
     *
1071
     * @return array An array of ReflectionProperty instances.
1072
     */
1073 99
    public function getReflectionProperties()
1074
    {
1075 99
        return $this->reflFields;
1076
    }
1077
1078
    /**
1079
     * Gets a ReflectionProperty for a specific field of the mapped class.
1080
     *
1081
     * @param string $name
1082
     *
1083
     * @return \ReflectionProperty
1084
     */
1085
    public function getReflectionProperty($name)
1086
    {
1087
        return $this->reflFields[$name];
1088
    }
1089
1090
    /**
1091
     * {@inheritDoc}
1092
     */
1093 1382
    public function getName()
1094
    {
1095 1382
        return $this->name;
1096
    }
1097
1098
    /**
1099
     * The namespace this Document class belongs to.
1100
     *
1101
     * @return string $namespace The namespace name.
1102
     */
1103
    public function getNamespace()
1104
    {
1105
        return $this->namespace;
1106
    }
1107
1108
    /**
1109
     * Returns the database this Document is mapped to.
1110
     *
1111
     * @return string $db The database name.
1112
     */
1113 1304
    public function getDatabase()
1114
    {
1115 1304
        return $this->db;
1116
    }
1117
1118
    /**
1119
     * Set the database this Document is mapped to.
1120
     *
1121
     * @param string $db The database name
1122
     */
1123 95
    public function setDatabase($db)
1124
    {
1125 95
        $this->db = $db;
1126 95
    }
1127
1128
    /**
1129
     * Get the collection this Document is mapped to.
1130
     *
1131
     * @return string $collection The collection name.
1132
     */
1133 1305
    public function getCollection()
1134
    {
1135 1305
        return $this->collection;
1136
    }
1137
1138
    /**
1139
     * Sets the collection this Document is mapped to.
1140
     *
1141
     * @param array|string $name
1142
     *
1143
     * @throws \InvalidArgumentException
1144
     */
1145 1492
    public function setCollection($name)
1146
    {
1147 1492
        if (is_array($name)) {
1148 1
            if (! isset($name['name'])) {
1149
                throw new \InvalidArgumentException('A name key is required when passing an array to setCollection()');
1150
            }
1151 1
            $this->collectionCapped = $name['capped'] ?? false;
1152 1
            $this->collectionSize = $name['size'] ?? 0;
1153 1
            $this->collectionMax = $name['max'] ?? 0;
1154 1
            $this->collection = $name['name'];
1155
        } else {
1156 1492
            $this->collection = $name;
1157
        }
1158 1492
    }
1159
1160
    /**
1161
     * Get whether or not the documents collection is capped.
1162
     *
1163
     * @return bool
1164
     */
1165 5
    public function getCollectionCapped()
1166
    {
1167 5
        return $this->collectionCapped;
1168
    }
1169
1170
    /**
1171
     * Set whether or not the documents collection is capped.
1172
     *
1173
     * @param bool $bool
1174
     */
1175 1
    public function setCollectionCapped($bool)
1176
    {
1177 1
        $this->collectionCapped = $bool;
1178 1
    }
1179
1180
    /**
1181
     * Get the collection size
1182
     *
1183
     * @return int
1184
     */
1185 5
    public function getCollectionSize()
1186
    {
1187 5
        return $this->collectionSize;
1188
    }
1189
1190
    /**
1191
     * Set the collection size.
1192
     *
1193
     * @param int $size
1194
     */
1195 1
    public function setCollectionSize($size)
1196
    {
1197 1
        $this->collectionSize = $size;
1198 1
    }
1199
1200
    /**
1201
     * Get the collection max.
1202
     *
1203
     * @return int
1204
     */
1205 5
    public function getCollectionMax()
1206
    {
1207 5
        return $this->collectionMax;
1208
    }
1209
1210
    /**
1211
     * Set the collection max.
1212
     *
1213
     * @param int $max
1214
     */
1215 1
    public function setCollectionMax($max)
1216
    {
1217 1
        $this->collectionMax = $max;
1218 1
    }
1219
1220
    /**
1221
     * Returns TRUE if this Document is mapped to a collection FALSE otherwise.
1222
     *
1223
     * @return bool
1224
     */
1225
    public function isMappedToCollection()
1226
    {
1227
        return $this->collection ? true : false;
1228
    }
1229
1230
    /**
1231
     * Validates the storage strategy of a mapping for consistency
1232
     * @param array $mapping
1233
     * @throws MappingException
1234
     */
1235 1401
    private function applyStorageStrategy(array &$mapping)
1236
    {
1237 1401
        if (! isset($mapping['type']) || isset($mapping['id'])) {
1238 1383
            return;
1239
        }
1240
1241
        switch (true) {
1242 1366
            case $mapping['type'] === 'int':
1243 1365
            case $mapping['type'] === 'float':
1244 832
                $defaultStrategy = self::STORAGE_STRATEGY_SET;
1245 832
                $allowedStrategies = [self::STORAGE_STRATEGY_SET, self::STORAGE_STRATEGY_INCREMENT];
1246 832
                break;
1247
1248 1365
            case $mapping['type'] === 'many':
1249 1081
                $defaultStrategy = CollectionHelper::DEFAULT_STRATEGY;
1250
                $allowedStrategies = [
1251 1081
                    self::STORAGE_STRATEGY_PUSH_ALL,
1252 1081
                    self::STORAGE_STRATEGY_ADD_TO_SET,
1253 1081
                    self::STORAGE_STRATEGY_SET,
1254 1081
                    self::STORAGE_STRATEGY_SET_ARRAY,
1255 1081
                    self::STORAGE_STRATEGY_ATOMIC_SET,
1256 1081
                    self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY,
1257
                ];
1258 1081
                break;
1259
1260
            default:
1261 1352
                $defaultStrategy = self::STORAGE_STRATEGY_SET;
1262 1352
                $allowedStrategies = [self::STORAGE_STRATEGY_SET];
1263
        }
1264
1265 1366
        if (! isset($mapping['strategy'])) {
1266 1357
            $mapping['strategy'] = $defaultStrategy;
1267
        }
1268
1269 1366
        if (! in_array($mapping['strategy'], $allowedStrategies)) {
1270
            throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']);
1271
        }
1272
1273 1366
        if (isset($mapping['reference']) && $mapping['type'] === 'many' && $mapping['isOwningSide']
1274 1366
            && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])) {
1275 1
            throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']);
1276
        }
1277 1365
    }
1278
1279
    /**
1280
     * Map a single embedded document.
1281
     *
1282
     * @param array $mapping The mapping information.
1283
     */
1284 6
    public function mapOneEmbedded(array $mapping)
1285
    {
1286 6
        $mapping['embedded'] = true;
1287 6
        $mapping['type'] = 'one';
1288 6
        $this->mapField($mapping);
1289 5
    }
1290
1291
    /**
1292
     * Map a collection of embedded documents.
1293
     *
1294
     * @param array $mapping The mapping information.
1295
     */
1296 5
    public function mapManyEmbedded(array $mapping)
1297
    {
1298 5
        $mapping['embedded'] = true;
1299 5
        $mapping['type'] = 'many';
1300 5
        $this->mapField($mapping);
1301 5
    }
1302
1303
    /**
1304
     * Map a single document reference.
1305
     *
1306
     * @param array $mapping The mapping information.
1307
     */
1308 2
    public function mapOneReference(array $mapping)
1309
    {
1310 2
        $mapping['reference'] = true;
1311 2
        $mapping['type'] = 'one';
1312 2
        $this->mapField($mapping);
1313 2
    }
1314
1315
    /**
1316
     * Map a collection of document references.
1317
     *
1318
     * @param array $mapping The mapping information.
1319
     */
1320 1
    public function mapManyReference(array $mapping)
1321
    {
1322 1
        $mapping['reference'] = true;
1323 1
        $mapping['type'] = 'many';
1324 1
        $this->mapField($mapping);
1325 1
    }
1326
1327
    /**
1328
     * INTERNAL:
1329
     * Adds a field mapping without completing/validating it.
1330
     * This is mainly used to add inherited field mappings to derived classes.
1331
     *
1332
     * @param array $fieldMapping
1333
     */
1334 119
    public function addInheritedFieldMapping(array $fieldMapping)
1335
    {
1336 119
        $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
1337
1338 119
        if (! isset($fieldMapping['association'])) {
1339 119
            return;
1340
        }
1341
1342 70
        $this->associationMappings[$fieldMapping['fieldName']] = $fieldMapping;
1343 70
    }
1344
1345
    /**
1346
     * INTERNAL:
1347
     * Adds an association mapping without completing/validating it.
1348
     * This is mainly used to add inherited association mappings to derived classes.
1349
     *
1350
     * @param array $mapping
1351
     *
1352
     *
1353
     * @throws MappingException
1354
     */
1355 71
    public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/)
1356
    {
1357 71
        $this->associationMappings[$mapping['fieldName']] = $mapping;
1358 71
    }
1359
1360
    /**
1361
     * Checks whether the class has a mapped association with the given field name.
1362
     *
1363
     * @param string $fieldName
1364
     * @return bool
1365
     */
1366 32
    public function hasReference($fieldName)
1367
    {
1368 32
        return isset($this->fieldMappings[$fieldName]['reference']);
1369
    }
1370
1371
    /**
1372
     * Checks whether the class has a mapped embed with the given field name.
1373
     *
1374
     * @param string $fieldName
1375
     * @return bool
1376
     */
1377 5
    public function hasEmbed($fieldName)
1378
    {
1379 5
        return isset($this->fieldMappings[$fieldName]['embedded']);
1380
    }
1381
1382
    /**
1383
     * {@inheritDoc}
1384
     *
1385
     * Checks whether the class has a mapped association (embed or reference) with the given field name.
1386
     */
1387 7
    public function hasAssociation($fieldName)
1388
    {
1389 7
        return $this->hasReference($fieldName) || $this->hasEmbed($fieldName);
1390
    }
1391
1392
    /**
1393
     * {@inheritDoc}
1394
     *
1395
     * Checks whether the class has a mapped reference or embed for the specified field and
1396
     * is a single valued association.
1397
     */
1398
    public function isSingleValuedAssociation($fieldName)
1399
    {
1400
        return $this->isSingleValuedReference($fieldName) || $this->isSingleValuedEmbed($fieldName);
1401
    }
1402
1403
    /**
1404
     * {@inheritDoc}
1405
     *
1406
     * Checks whether the class has a mapped reference or embed for the specified field and
1407
     * is a collection valued association.
1408
     */
1409
    public function isCollectionValuedAssociation($fieldName)
1410
    {
1411
        return $this->isCollectionValuedReference($fieldName) || $this->isCollectionValuedEmbed($fieldName);
1412
    }
1413
1414
    /**
1415
     * Checks whether the class has a mapped association for the specified field
1416
     * and if yes, checks whether it is a single-valued association (to-one).
1417
     *
1418
     * @param string $fieldName
1419
     * @return bool TRUE if the association exists and is single-valued, FALSE otherwise.
1420
     */
1421
    public function isSingleValuedReference($fieldName)
1422
    {
1423
        return isset($this->fieldMappings[$fieldName]['association']) &&
1424
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_ONE;
1425
    }
1426
1427
    /**
1428
     * Checks whether the class has a mapped association for the specified field
1429
     * and if yes, checks whether it is a collection-valued association (to-many).
1430
     *
1431
     * @param string $fieldName
1432
     * @return bool TRUE if the association exists and is collection-valued, FALSE otherwise.
1433
     */
1434
    public function isCollectionValuedReference($fieldName)
1435
    {
1436
        return isset($this->fieldMappings[$fieldName]['association']) &&
1437
            $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_MANY;
1438
    }
1439
1440
    /**
1441
     * Checks whether the class has a mapped embedded document for the specified field
1442
     * and if yes, checks whether it is a single-valued association (to-one).
1443
     *
1444
     * @param string $fieldName
1445
     * @return bool TRUE if the association exists and is single-valued, FALSE otherwise.
1446
     */
1447
    public function isSingleValuedEmbed($fieldName)
1448
    {
1449
        return isset($this->fieldMappings[$fieldName]['association']) &&
1450
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_ONE;
1451
    }
1452
1453
    /**
1454
     * Checks whether the class has a mapped embedded document for the specified field
1455
     * and if yes, checks whether it is a collection-valued association (to-many).
1456
     *
1457
     * @param string $fieldName
1458
     * @return bool TRUE if the association exists and is collection-valued, FALSE otherwise.
1459
     */
1460
    public function isCollectionValuedEmbed($fieldName)
1461
    {
1462
        return isset($this->fieldMappings[$fieldName]['association']) &&
1463
            $this->fieldMappings[$fieldName]['association'] === self::EMBED_MANY;
1464
    }
1465
1466
    /**
1467
     * Sets the ID generator used to generate IDs for instances of this class.
1468
     *
1469
     * @param AbstractIdGenerator $generator
1470
     */
1471 1318
    public function setIdGenerator($generator)
1472
    {
1473 1318
        $this->idGenerator = $generator;
1474 1318
    }
1475
1476
    /**
1477
     * Casts the identifier to its portable PHP type.
1478
     *
1479
     * @param mixed $id
1480
     * @return mixed $id
1481
     */
1482 598
    public function getPHPIdentifierValue($id)
1483
    {
1484 598
        $idType = $this->fieldMappings[$this->identifier]['type'];
1485 598
        return Type::getType($idType)->convertToPHPValue($id);
1486
    }
1487
1488
    /**
1489
     * Casts the identifier to its database type.
1490
     *
1491
     * @param mixed $id
1492
     * @return mixed $id
1493
     */
1494 662
    public function getDatabaseIdentifierValue($id)
1495
    {
1496 662
        $idType = $this->fieldMappings[$this->identifier]['type'];
1497 662
        return Type::getType($idType)->convertToDatabaseValue($id);
1498
    }
1499
1500
    /**
1501
     * Sets the document identifier of a document.
1502
     *
1503
     * The value will be converted to a PHP type before being set.
1504
     *
1505
     * @param object $document
1506
     * @param mixed  $id
1507
     */
1508 522
    public function setIdentifierValue($document, $id)
1509
    {
1510 522
        $id = $this->getPHPIdentifierValue($id);
1511 522
        $this->reflFields[$this->identifier]->setValue($document, $id);
1512 522
    }
1513
1514
    /**
1515
     * Gets the document identifier as a PHP type.
1516
     *
1517
     * @param object $document
1518
     * @return mixed $id
1519
     */
1520 605
    public function getIdentifierValue($document)
1521
    {
1522 605
        return $this->reflFields[$this->identifier]->getValue($document);
1523
    }
1524
1525
    /**
1526
     * {@inheritDoc}
1527
     *
1528
     * Since MongoDB only allows exactly one identifier field this is a proxy
1529
     * to {@see getIdentifierValue()} and returns an array with the identifier
1530
     * field as a key.
1531
     */
1532
    public function getIdentifierValues($object)
1533
    {
1534
        return [$this->identifier => $this->getIdentifierValue($object)];
1535
    }
1536
1537
    /**
1538
     * Get the document identifier object as a database type.
1539
     *
1540
     * @param object $document
1541
     *
1542
     * @return ObjectId $id The ObjectId
1543
     */
1544 31
    public function getIdentifierObject($document)
1545
    {
1546 31
        return $this->getDatabaseIdentifierValue($this->getIdentifierValue($document));
1547
    }
1548
1549
    /**
1550
     * Sets the specified field to the specified value on the given document.
1551
     *
1552
     * @param object $document
1553
     * @param string $field
1554
     * @param mixed  $value
1555
     */
1556 8
    public function setFieldValue($document, $field, $value)
1557
    {
1558 8
        if ($document instanceof Proxy && ! $document->__isInitialized()) {
1559
            //property changes to an uninitialized proxy will not be tracked or persisted,
1560
            //so the proxy needs to be loaded first.
1561 1
            $document->__load();
1562
        }
1563
1564 8
        $this->reflFields[$field]->setValue($document, $value);
1565 8
    }
1566
1567
    /**
1568
     * Gets the specified field's value off the given document.
1569
     *
1570
     * @param object $document
1571
     * @param string $field
1572
     *
1573
     * @return mixed
1574
     */
1575 27
    public function getFieldValue($document, $field)
1576
    {
1577 27
        if ($document instanceof Proxy && $field !== $this->identifier && ! $document->__isInitialized()) {
1578 1
            $document->__load();
1579
        }
1580
1581 27
        return $this->reflFields[$field]->getValue($document);
1582
    }
1583
1584
    /**
1585
     * Gets the mapping of a field.
1586
     *
1587
     * @param string $fieldName The field name.
1588
     *
1589
     * @return array  The field mapping.
1590
     *
1591
     * @throws MappingException If the $fieldName is not found in the fieldMappings array.
1592
     */
1593 169
    public function getFieldMapping($fieldName)
1594
    {
1595 169
        if (! isset($this->fieldMappings[$fieldName])) {
1596 6
            throw MappingException::mappingNotFound($this->name, $fieldName);
1597
        }
1598 167
        return $this->fieldMappings[$fieldName];
1599
    }
1600
1601
    /**
1602
     * Gets mappings of fields holding embedded document(s).
1603
     *
1604
     * @return array of field mappings
1605
     */
1606 560
    public function getEmbeddedFieldsMappings()
1607
    {
1608 560
        return array_filter(
1609 560
            $this->associationMappings,
1610
            function ($assoc) {
1611 425
                return ! empty($assoc['embedded']);
1612 560
            }
1613
        );
1614
    }
1615
1616
    /**
1617
     * Gets the field mapping by its DB name.
1618
     * E.g. it returns identifier's mapping when called with _id.
1619
     *
1620
     * @param string $dbFieldName
1621
     *
1622
     * @return array
1623
     * @throws MappingException
1624
     */
1625 7
    public function getFieldMappingByDbFieldName($dbFieldName)
1626
    {
1627 7
        foreach ($this->fieldMappings as $mapping) {
1628 7
            if ($mapping['name'] === $dbFieldName) {
1629 7
                return $mapping;
1630
            }
1631
        }
1632
1633
        throw MappingException::mappingNotFoundByDbName($this->name, $dbFieldName);
1634
    }
1635
1636
    /**
1637
     * Check if the field is not null.
1638
     *
1639
     * @param string $fieldName The field name
1640
     *
1641
     * @return bool  TRUE if the field is not null, FALSE otherwise.
1642
     */
1643 1
    public function isNullable($fieldName)
1644
    {
1645 1
        $mapping = $this->getFieldMapping($fieldName);
1646 1
        if ($mapping !== false) {
1647 1
            return isset($mapping['nullable']) && $mapping['nullable'] === true;
1648
        }
1649
        return false;
1650
    }
1651
1652
    /**
1653
     * Checks whether the document has a discriminator field and value configured.
1654
     *
1655
     * @return bool
1656
     */
1657 499
    public function hasDiscriminator()
1658
    {
1659 499
        return isset($this->discriminatorField, $this->discriminatorValue);
1660
    }
1661
1662
    /**
1663
     * Sets the type of Id generator to use for the mapped class.
1664
     *
1665
     * @param string $generatorType Generator type.
1666
     */
1667 891
    public function setIdGeneratorType($generatorType)
1668
    {
1669 891
        $this->generatorType = $generatorType;
1670 891
    }
1671
1672
    /**
1673
     * Sets the Id generator options.
1674
     *
1675
     * @param array $generatorOptions Generator options.
1676
     */
1677
    public function setIdGeneratorOptions($generatorOptions)
1678
    {
1679
        $this->generatorOptions = $generatorOptions;
1680
    }
1681
1682
    /**
1683
     * @return bool
1684
     */
1685 568
    public function isInheritanceTypeNone()
1686
    {
1687 568
        return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
1688
    }
1689
1690
    /**
1691
     * Checks whether the mapped class uses the SINGLE_COLLECTION inheritance mapping strategy.
1692
     *
1693
     * @return bool
1694
     */
1695 890
    public function isInheritanceTypeSingleCollection()
1696
    {
1697 890
        return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION;
1698
    }
1699
1700
    /**
1701
     * Checks whether the mapped class uses the COLLECTION_PER_CLASS inheritance mapping strategy.
1702
     *
1703
     * @return bool
1704
     */
1705
    public function isInheritanceTypeCollectionPerClass()
1706
    {
1707
        return $this->inheritanceType === self::INHERITANCE_TYPE_COLLECTION_PER_CLASS;
1708
    }
1709
1710
    /**
1711
     * Sets the mapped subclasses of this class.
1712
     *
1713
     * @param string[] $subclasses The names of all mapped subclasses.
1714
     */
1715 2
    public function setSubclasses(array $subclasses)
1716
    {
1717 2
        foreach ($subclasses as $subclass) {
1718 2
            if (strpos($subclass, '\\') === false && strlen($this->namespace)) {
1719 1
                $this->subClasses[] = $this->namespace . '\\' . $subclass;
1720
            } else {
1721 2
                $this->subClasses[] = $subclass;
1722
            }
1723
        }
1724 2
    }
1725
1726
    /**
1727
     * Sets the parent class names.
1728
     * Assumes that the class names in the passed array are in the order:
1729
     * directParent -> directParentParent -> directParentParentParent ... -> root.
1730
     *
1731
     * @param string[] $classNames
1732
     */
1733 1373
    public function setParentClasses(array $classNames)
1734
    {
1735 1373
        $this->parentClasses = $classNames;
1736
1737 1373
        if (count($classNames) <= 0) {
1738 1372
            return;
1739
        }
1740
1741 103
        $this->rootDocumentName = array_pop($classNames);
1742 103
    }
1743
1744
    /**
1745
     * Checks whether the class will generate a new \MongoDB\BSON\ObjectId instance for us.
1746
     *
1747
     * @return bool TRUE if the class uses the AUTO generator, FALSE otherwise.
1748
     */
1749
    public function isIdGeneratorAuto()
1750
    {
1751
        return $this->generatorType === self::GENERATOR_TYPE_AUTO;
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->generatorType (string) and self::GENERATOR_TYPE_AUTO (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1752
    }
1753
1754
    /**
1755
     * Checks whether the class will use a collection to generate incremented identifiers.
1756
     *
1757
     * @return bool TRUE if the class uses the INCREMENT generator, FALSE otherwise.
1758
     */
1759
    public function isIdGeneratorIncrement()
1760
    {
1761
        return $this->generatorType === self::GENERATOR_TYPE_INCREMENT;
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->generatorType (string) and self::GENERATOR_TYPE_INCREMENT (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1762
    }
1763
1764
    /**
1765
     * Checks whether the class will generate a uuid id.
1766
     *
1767
     * @return bool TRUE if the class uses the UUID generator, FALSE otherwise.
1768
     */
1769
    public function isIdGeneratorUuid()
1770
    {
1771
        return $this->generatorType === self::GENERATOR_TYPE_UUID;
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->generatorType (string) and self::GENERATOR_TYPE_UUID (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1772
    }
1773
1774
    /**
1775
     * Checks whether the class uses no id generator.
1776
     *
1777
     * @return bool TRUE if the class does not use any id generator, FALSE otherwise.
1778
     */
1779
    public function isIdGeneratorNone()
1780
    {
1781
        return $this->generatorType === self::GENERATOR_TYPE_NONE;
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $this->generatorType (string) and self::GENERATOR_TYPE_NONE (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
1782
    }
1783
1784
    /**
1785
     * Sets the version field mapping used for versioning. Sets the default
1786
     * value to use depending on the column type.
1787
     *
1788
     * @param array $mapping The version field mapping array
1789
     *
1790
     * @throws LockException
1791
     */
1792 70
    public function setVersionMapping(array &$mapping)
1793
    {
1794 70
        if ($mapping['type'] !== 'int' && $mapping['type'] !== 'date') {
1795 1
            throw LockException::invalidVersionFieldType($mapping['type']);
1796
        }
1797
1798 69
        $this->isVersioned  = true;
1799 69
        $this->versionField = $mapping['fieldName'];
1800 69
    }
1801
1802
    /**
1803
     * Sets whether this class is to be versioned for optimistic locking.
1804
     *
1805
     * @param bool $bool
1806
     */
1807 891
    public function setVersioned($bool)
1808
    {
1809 891
        $this->isVersioned = $bool;
1810 891
    }
1811
1812
    /**
1813
     * Sets the name of the field that is to be used for versioning if this class is
1814
     * versioned for optimistic locking.
1815
     *
1816
     * @param string $versionField
1817
     */
1818 891
    public function setVersionField($versionField)
1819
    {
1820 891
        $this->versionField = $versionField;
1821 891
    }
1822
1823
    /**
1824
     * Sets the version field mapping used for versioning. Sets the default
1825
     * value to use depending on the column type.
1826
     *
1827
     * @param array $mapping The version field mapping array
1828
     *
1829
     * @throws LockException
1830
     */
1831 22
    public function setLockMapping(array &$mapping)
1832
    {
1833 22
        if ($mapping['type'] !== 'int') {
1834 1
            throw LockException::invalidLockFieldType($mapping['type']);
1835
        }
1836
1837 21
        $this->isLockable = true;
1838 21
        $this->lockField = $mapping['fieldName'];
1839 21
    }
1840
1841
    /**
1842
     * Sets whether this class is to allow pessimistic locking.
1843
     *
1844
     * @param bool $bool
1845
     */
1846
    public function setLockable($bool)
1847
    {
1848
        $this->isLockable = $bool;
1849
    }
1850
1851
    /**
1852
     * Sets the name of the field that is to be used for storing whether a document
1853
     * is currently locked or not.
1854
     *
1855
     * @param string $lockField
1856
     */
1857
    public function setLockField($lockField)
1858
    {
1859
        $this->lockField = $lockField;
1860
    }
1861
1862
    /**
1863
     * Marks this class as read only, no change tracking is applied to it.
1864
     */
1865 5
    public function markReadOnly()
1866
    {
1867 5
        $this->isReadOnly = true;
1868 5
    }
1869
1870
    /**
1871
     * {@inheritDoc}
1872
     */
1873
    public function getFieldNames()
1874
    {
1875
        return array_keys($this->fieldMappings);
1876
    }
1877
1878
    /**
1879
     * {@inheritDoc}
1880
     */
1881
    public function getAssociationNames()
1882
    {
1883
        return array_keys($this->associationMappings);
1884
    }
1885
1886
    /**
1887
     * {@inheritDoc}
1888
     */
1889 23
    public function getTypeOfField($fieldName)
1890
    {
1891 23
        return isset($this->fieldMappings[$fieldName]) ?
1892 23
            $this->fieldMappings[$fieldName]['type'] : null;
1893
    }
1894
1895
    /**
1896
     * {@inheritDoc}
1897
     */
1898 4
    public function getAssociationTargetClass($assocName)
1899
    {
1900 4
        if (! isset($this->associationMappings[$assocName])) {
1901 2
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1902
        }
1903
1904 2
        return $this->associationMappings[$assocName]['targetDocument'];
1905
    }
1906
1907
    /**
1908
     * Retrieve the collectionClass associated with an association
1909
     *
1910
     * @param string $assocName
1911
     */
1912 1
    public function getAssociationCollectionClass($assocName)
1913
    {
1914 1
        if (! isset($this->associationMappings[$assocName])) {
1915
            throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
1916
        }
1917
1918 1
        if (! array_key_exists('collectionClass', $this->associationMappings[$assocName])) {
1919
            throw new InvalidArgumentException("collectionClass can only be applied to 'embedMany' and 'referenceMany' associations.");
1920
        }
1921
1922 1
        return $this->associationMappings[$assocName]['collectionClass'];
1923
    }
1924
1925
    /**
1926
     * {@inheritDoc}
1927
     */
1928
    public function isAssociationInverseSide($fieldName)
1929
    {
1930
        throw new \BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1931
    }
1932
1933
    /**
1934
     * {@inheritDoc}
1935
     */
1936
    public function getAssociationMappedByTargetField($fieldName)
1937
    {
1938
        throw new \BadMethodCallException(__METHOD__ . '() is not implemented yet.');
1939
    }
1940
1941
    /**
1942
     * Map a field.
1943
     *
1944
     * @param array $mapping The mapping information.
1945
     *
1946
     * @return array
1947
     *
1948
     * @throws MappingException
1949
     */
1950 1417
    public function mapField(array $mapping)
1951
    {
1952 1417
        if (! isset($mapping['fieldName']) && isset($mapping['name'])) {
1953 9
            $mapping['fieldName'] = $mapping['name'];
1954
        }
1955 1417
        if (! isset($mapping['fieldName'])) {
1956
            throw MappingException::missingFieldName($this->name);
1957
        }
1958 1417
        if (! isset($mapping['name'])) {
1959 1408
            $mapping['name'] = $mapping['fieldName'];
1960
        }
1961 1417
        if ($this->identifier === $mapping['name'] && empty($mapping['id'])) {
1962 1
            throw MappingException::mustNotChangeIdentifierFieldsType($this->name, $mapping['name']);
1963
        }
1964 1416
        if ($this->discriminatorField !== null && $this->discriminatorField === $mapping['name']) {
1965 1
            throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField);
1966
        }
1967 1415
        if (isset($mapping['targetDocument']) && strpos($mapping['targetDocument'], '\\') === false && strlen($this->namespace)) {
1968 1108
            $mapping['targetDocument'] = $this->namespace . '\\' . $mapping['targetDocument'];
1969
        }
1970 1415
        if (isset($mapping['collectionClass'])) {
1971 56
            if (strpos($mapping['collectionClass'], '\\') === false && strlen($this->namespace)) {
1972 54
                $mapping['collectionClass'] = $this->namespace . '\\' . $mapping['collectionClass'];
1973
            }
1974 56
            $mapping['collectionClass'] = ltrim($mapping['collectionClass'], '\\');
1975
        }
1976 1415
        if (! empty($mapping['collectionClass'])) {
1977 56
            $rColl = new \ReflectionClass($mapping['collectionClass']);
1978 56
            if (! $rColl->implementsInterface('Doctrine\\Common\\Collections\\Collection')) {
1979 1
                throw MappingException::collectionClassDoesNotImplementCommonInterface($this->name, $mapping['fieldName'], $mapping['collectionClass']);
1980
            }
1981
        }
1982
1983 1414
        if (isset($mapping['discriminatorMap'])) {
1984 117
            foreach ($mapping['discriminatorMap'] as $key => $class) {
1985 117
                if (strpos($class, '\\') !== false || ! strlen($this->namespace)) {
1986 97
                    continue;
1987
                }
1988
1989 66
                $mapping['discriminatorMap'][$key] = $this->namespace . '\\' . $class;
1990
            }
1991
        }
1992
1993 1414
        if (isset($mapping['cascade']) && isset($mapping['embedded'])) {
1994 1
            throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']);
1995
        }
1996
1997 1413
        $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : [];
1998
1999 1413
        if (in_array('all', $cascades) || isset($mapping['embedded'])) {
2000 1119
            $cascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
2001
        }
2002
2003 1413
        if (isset($mapping['embedded'])) {
2004 1080
            unset($mapping['cascade']);
2005 1408
        } elseif (isset($mapping['cascade'])) {
2006 909
            $mapping['cascade'] = $cascades;
2007
        }
2008
2009 1413
        $mapping['isCascadeRemove'] = in_array('remove', $cascades);
2010 1413
        $mapping['isCascadePersist'] = in_array('persist', $cascades);
2011 1413
        $mapping['isCascadeRefresh'] = in_array('refresh', $cascades);
2012 1413
        $mapping['isCascadeMerge'] = in_array('merge', $cascades);
2013 1413
        $mapping['isCascadeDetach'] = in_array('detach', $cascades);
2014
2015 1413
        if (isset($mapping['id']) && $mapping['id'] === true) {
2016 1381
            $mapping['name'] = '_id';
2017 1381
            $this->identifier = $mapping['fieldName'];
2018 1381
            if (isset($mapping['strategy'])) {
2019 1374
                $this->generatorType = constant(self::class . '::GENERATOR_TYPE_' . strtoupper($mapping['strategy']));
2020
            }
2021 1381
            $this->generatorOptions = $mapping['options'] ?? [];
2022 1381
            switch ($this->generatorType) {
2023 1381
                case self::GENERATOR_TYPE_AUTO:
2024 1309
                    $mapping['type'] = 'id';
2025 1309
                    break;
2026
                default:
2027 148
                    if (! empty($this->generatorOptions['type'])) {
2028 56
                        $mapping['type'] = $this->generatorOptions['type'];
2029 92
                    } elseif (empty($mapping['type'])) {
2030 80
                        $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? 'int_id' : 'custom_id';
2031
                    }
2032
            }
2033 1381
            unset($this->generatorOptions['type']);
2034
        }
2035
2036 1413
        if (! isset($mapping['nullable'])) {
2037 40
            $mapping['nullable'] = false;
2038
        }
2039
2040 1413
        if (isset($mapping['reference'])
2041 1413
            && isset($mapping['storeAs'])
2042 1413
            && $mapping['storeAs'] === self::REFERENCE_STORE_AS_ID
2043 1413
            && ! isset($mapping['targetDocument'])
2044
        ) {
2045 3
            throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']);
2046
        }
2047
2048 1410
        if (isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) &&
2049 1410
                (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))) {
2050 4
            throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']);
2051
        }
2052
2053 1406
        if ($this->isEmbeddedDocument && $mapping['type'] === 'many' && isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2054 1
            throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']);
2055
        }
2056
2057 1405
        if (isset($mapping['repositoryMethod']) && ! (empty($mapping['skip']) && empty($mapping['limit']) && empty($mapping['sort']))) {
2058 3
            throw MappingException::repositoryMethodCanNotBeCombinedWithSkipLimitAndSort($this->name, $mapping['fieldName']);
2059
        }
2060
2061 1402
        if (isset($mapping['reference']) && $mapping['type'] === 'one') {
2062 1002
            $mapping['association'] = self::REFERENCE_ONE;
2063
        }
2064 1402
        if (isset($mapping['reference']) && $mapping['type'] === 'many') {
2065 956
            $mapping['association'] = self::REFERENCE_MANY;
2066
        }
2067 1402
        if (isset($mapping['embedded']) && $mapping['type'] === 'one') {
2068 957
            $mapping['association'] = self::EMBED_ONE;
2069
        }
2070 1402
        if (isset($mapping['embedded']) && $mapping['type'] === 'many') {
2071 981
            $mapping['association'] = self::EMBED_MANY;
2072
        }
2073
2074 1402
        if (isset($mapping['association']) && ! isset($mapping['targetDocument']) && ! isset($mapping['discriminatorField'])) {
2075 126
            $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD;
2076
        }
2077
2078 1402
        if (isset($mapping['version'])) {
2079 70
            $mapping['notSaved'] = true;
2080 70
            $this->setVersionMapping($mapping);
2081
        }
2082 1402
        if (isset($mapping['lock'])) {
2083 22
            $mapping['notSaved'] = true;
2084 22
            $this->setLockMapping($mapping);
2085
        }
2086 1402
        $mapping['isOwningSide'] = true;
2087 1402
        $mapping['isInverseSide'] = false;
2088 1402
        if (isset($mapping['reference'])) {
2089 1067
            if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
2090 87
                $mapping['isOwningSide'] = true;
2091 87
                $mapping['isInverseSide'] = false;
2092
            }
2093 1067
            if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
2094 818
                $mapping['isInverseSide'] = true;
2095 818
                $mapping['isOwningSide'] = false;
2096
            }
2097 1067
            if (isset($mapping['repositoryMethod'])) {
2098 61
                $mapping['isInverseSide'] = true;
2099 61
                $mapping['isOwningSide'] = false;
2100
            }
2101 1067
            if (! isset($mapping['orphanRemoval'])) {
2102 1049
                $mapping['orphanRemoval'] = false;
2103
            }
2104
        }
2105
2106 1402
        if (! empty($mapping['prime']) && ($mapping['association'] !== self::REFERENCE_MANY || ! $mapping['isInverseSide'])) {
2107
            throw MappingException::referencePrimersOnlySupportedForInverseReferenceMany($this->name, $mapping['fieldName']);
2108
        }
2109
2110 1402
        if ($this->isFile && ! $this->isAllowedGridFSField($mapping['name'])) {
2111 1
            throw MappingException::fieldNotAllowedForGridFS($this->name, $mapping['fieldName']);
2112
        }
2113
2114 1401
        $this->applyStorageStrategy($mapping);
2115
2116 1400
        $this->fieldMappings[$mapping['fieldName']] = $mapping;
2117 1400
        if (isset($mapping['association'])) {
2118 1209
            $this->associationMappings[$mapping['fieldName']] = $mapping;
2119
        }
2120
2121 1400
        $reflProp = $this->reflClass->getProperty($mapping['fieldName']);
2122 1399
        $reflProp->setAccessible(true);
2123 1399
        $this->reflFields[$mapping['fieldName']] = $reflProp;
2124
2125 1399
        return $mapping;
2126
    }
2127
2128
    /**
2129
     * Determines which fields get serialized.
2130
     *
2131
     * It is only serialized what is necessary for best unserialization performance.
2132
     * That means any metadata properties that are not set or empty or simply have
2133
     * their default value are NOT serialized.
2134
     *
2135
     * Parts that are also NOT serialized because they can not be properly unserialized:
2136
     *      - reflClass (ReflectionClass)
2137
     *      - reflFields (ReflectionProperty array)
2138
     *
2139
     * @return array The names of all the fields that should be serialized.
2140
     */
2141 6
    public function __sleep()
2142
    {
2143
        // This metadata is always serialized/cached.
2144
        $serialized = [
2145 6
            'fieldMappings',
2146
            'associationMappings',
2147
            'identifier',
2148
            'name',
2149
            'namespace', // TODO: REMOVE
2150
            'db',
2151
            'collection',
2152
            'readPreference',
2153
            'readPreferenceTags',
2154
            'writeConcern',
2155
            'rootDocumentName',
2156
            'generatorType',
2157
            'generatorOptions',
2158
            'idGenerator',
2159
            'indexes',
2160
            'shardKey',
2161
        ];
2162
2163
        // The rest of the metadata is only serialized if necessary.
2164 6
        if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
2165
            $serialized[] = 'changeTrackingPolicy';
2166
        }
2167
2168 6
        if ($this->customRepositoryClassName) {
2169 1
            $serialized[] = 'customRepositoryClassName';
2170
        }
2171
2172 6
        if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE || $this->discriminatorField !== null) {
2173 4
            $serialized[] = 'inheritanceType';
2174 4
            $serialized[] = 'discriminatorField';
2175 4
            $serialized[] = 'discriminatorValue';
2176 4
            $serialized[] = 'discriminatorMap';
2177 4
            $serialized[] = 'defaultDiscriminatorValue';
2178 4
            $serialized[] = 'parentClasses';
2179 4
            $serialized[] = 'subClasses';
2180
        }
2181
2182 6
        if ($this->isMappedSuperclass) {
2183 1
            $serialized[] = 'isMappedSuperclass';
2184
        }
2185
2186 6
        if ($this->isEmbeddedDocument) {
2187 1
            $serialized[] = 'isEmbeddedDocument';
2188
        }
2189
2190 6
        if ($this->isQueryResultDocument) {
2191
            $serialized[] = 'isQueryResultDocument';
2192
        }
2193
2194 6
        if ($this->isFile) {
2195
            $serialized[] = 'isFile';
2196
        }
2197
2198 6
        if ($this->isVersioned) {
2199
            $serialized[] = 'isVersioned';
2200
            $serialized[] = 'versionField';
2201
        }
2202
2203 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...
2204
            $serialized[] = 'lifecycleCallbacks';
2205
        }
2206
2207 6
        if ($this->collectionCapped) {
2208 1
            $serialized[] = 'collectionCapped';
2209 1
            $serialized[] = 'collectionSize';
2210 1
            $serialized[] = 'collectionMax';
2211
        }
2212
2213 6
        if ($this->isReadOnly) {
2214
            $serialized[] = 'isReadOnly';
2215
        }
2216
2217 6
        return $serialized;
2218
    }
2219
2220
    /**
2221
     * Restores some state that can not be serialized/unserialized.
2222
     *
2223
     */
2224 6
    public function __wakeup()
2225
    {
2226
        // Restore ReflectionClass and properties
2227 6
        $this->reflClass = new \ReflectionClass($this->name);
2228 6
        $this->instantiator = $this->instantiator ?: new Instantiator();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->instantiator ?: n...antiator\Instantiator() can also be of type object<Doctrine\Instantiator\Instantiator>. However, the property $instantiator is declared as type object<Doctrine\Instanti...antiatorInterface>|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2229
2230 6
        foreach ($this->fieldMappings as $field => $mapping) {
2231 3
            if (isset($mapping['declared'])) {
2232 1
                $reflField = new \ReflectionProperty($mapping['declared'], $field);
2233
            } else {
2234 3
                $reflField = $this->reflClass->getProperty($field);
2235
            }
2236 3
            $reflField->setAccessible(true);
2237 3
            $this->reflFields[$field] = $reflField;
2238
        }
2239 6
    }
2240
2241
    /**
2242
     * Creates a new instance of the mapped class, without invoking the constructor.
2243
     *
2244
     * @return object
2245
     */
2246 361
    public function newInstance()
2247
    {
2248 361
        return $this->instantiator->instantiate($this->name);
2249
    }
2250
2251 59
    private function isAllowedGridFSField(string $name): bool
2252
    {
2253 59
        return in_array($name, ['_id', 'chunkSize', 'filename', 'length', 'metadata', 'uploadDate'], true);
2254
    }
2255
}
2256