Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
created

lib/Doctrine/ORM/Mapping/ClassMetadata.php (5 issues)

1
<?php
2
3
4
declare(strict_types=1);
5
6
namespace Doctrine\ORM\Mapping;
7
8
use Doctrine\ORM\Cache\CacheException;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\Factory\NamingStrategy;
11
use Doctrine\ORM\Reflection\ReflectionService;
12
use Doctrine\ORM\Sequencing\Planning\ValueGenerationPlan;
13
use Doctrine\ORM\Utility\PersisterHelper;
14
15
/**
16
 * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
17
 * of an entity and its associations.
18
 *
19
 * @author Roman Borschel <[email protected]>
20
 * @author Jonathan H. Wage <[email protected]>
21
 * @author Guilherme Blanco <[email protected]>
22
 * @since 2.0
23
 */
24
class ClassMetadata extends ComponentMetadata implements TableOwner
25
{
26
    /**
27
     * The name of the custom repository class used for the entity class.
28
     * (Optional).
29
     *
30
     * @var string
31
     */
32
    protected $customRepositoryClassName;
33
34
    /**
35
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
36
     *
37
     * @var boolean
38
     */
39
    public $isMappedSuperclass = false;
40
41
    /**
42
     * READ-ONLY: Whether this class describes the mapping of an embeddable class.
43
     *
44
     * @var boolean
45
     */
46
    public $isEmbeddedClass = false;
47
48
    /**
49
     * Whether this class describes the mapping of a read-only class.
50
     * That means it is never considered for change-tracking in the UnitOfWork.
51
     * It is a very helpful performance optimization for entities that are immutable,
52
     * either in your domain or through the relation database (coming from a view,
53
     * or a history table for example).
54
     *
55
     * @var boolean
56
     */
57
    private $readOnly = false;
58
59
    /**
60
     * The names of all subclasses (descendants).
61
     *
62
     * @var array
63
     */
64
    protected $subClasses = [];
65
66
    /**
67
     * READ-ONLY: The names of all embedded classes based on properties.
68
     *
69
     * @var array
70
     */
71
    //public $embeddedClasses = [];
72
73
    /**
74
     * The named queries allowed to be called directly from Repository.
75
     *
76
     * @var array
77
     */
78
    protected $namedQueries = [];
79
80
    /**
81
     * READ-ONLY: The named native queries allowed to be called directly from Repository.
82
     *
83
     * A native SQL named query definition has the following structure:
84
     * <pre>
85
     * array(
86
     *     'name'               => <query name>,
87
     *     'query'              => <sql query>,
88
     *     'resultClass'        => <class of the result>,
89
     *     'resultSetMapping'   => <name of a SqlResultSetMapping>
90
     * )
91
     * </pre>
92
     *
93
     * @var array
94
     */
95
    public $namedNativeQueries = [];
96
97
    /**
98
     * READ-ONLY: The mappings of the results of native SQL queries.
99
     *
100
     * A native result mapping definition has the following structure:
101
     * <pre>
102
     * array(
103
     *     'name'               => <result name>,
104
     *     'entities'           => array(<entity result mapping>),
105
     *     'columns'            => array(<column result mapping>)
106
     * )
107
     * </pre>
108
     *
109
     * @var array
110
     */
111
    public $sqlResultSetMappings = [];
112
113
    /**
114
     * READ-ONLY: The registered lifecycle callbacks for entities of this class.
115
     *
116
     * @var array<string, array<string>>
117
     */
118
    public $lifecycleCallbacks = [];
119
120
    /**
121
     * READ-ONLY: The registered entity listeners.
122
     *
123
     * @var array
124
     */
125
    public $entityListeners = [];
126
127
    /**
128
     * READ-ONLY: The field names of all fields that are part of the identifier/primary key
129
     * of the mapped entity class.
130
     *
131
     * @var string[]
132
     */
133
    public $identifier = [];
134
135
    /**
136
     * READ-ONLY: The inheritance mapping type used by the class.
137
     *
138
     * @var string
139
     */
140
    public $inheritanceType = InheritanceType::NONE;
141
142
    /**
143
     * READ-ONLY: The policy used for change-tracking on entities of this class.
144
     *
145
     * @var string
146
     */
147
    public $changeTrackingPolicy = ChangeTrackingPolicy::DEFERRED_IMPLICIT;
148
149
    /**
150
     * READ-ONLY: The discriminator value of this class.
151
     *
152
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
153
     * where a discriminator column is used.</b>
154
     *
155
     * @var mixed
156
     *
157
     * @see discriminatorColumn
158
     */
159
    public $discriminatorValue;
160
161
    /**
162
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
163
     *
164
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
165
     * where a discriminator column is used.</b>
166
     *
167
     * @var array<string, string>
168
     *
169
     * @see discriminatorColumn
170
     */
171
    public $discriminatorMap = [];
172
173
    /**
174
     * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
175
     * inheritance mappings.
176
     *
177
     * @var DiscriminatorColumnMetadata
178
     */
179
    public $discriminatorColumn;
180
181
    /**
182
     * READ-ONLY: The primary table metadata.
183
     *
184
     * @var TableMetadata
185
     */
186
    public $table;
187
188
    /**
189
     * READ-ONLY: An array of field names. Used to look up field names from column names.
190
     * Keys are column names and values are field names.
191
     *
192
     * @var array<string, string>
193
     */
194
    public $fieldNames = [];
195
196
    /**
197
     * READ-ONLY: The field which is used for versioning in optimistic locking (if any).
198
     *
199
     * @var FieldMetadata|null
200
     */
201
    public $versionProperty;
202
203
    /**
204
     * NamingStrategy determining the default column and table names.
205
     *
206
     * @var NamingStrategy
207
     */
208
    protected $namingStrategy;
209
210
    /**
211
     * Value generation plan is responsible for generating values for auto-generated fields.
212
     *
213
     * @var ValueGenerationPlan
214
     */
215
    protected $valueGenerationPlan;
216
217
    /**
218
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
219
     * metadata of the class with the given name.
220
     *
221
     * @param string                       $entityName              The name of the entity class.
222
     * @param ClassMetadataBuildingContext $metadataBuildingContext
223
     */
224 471
    public function __construct(
225
        string $entityName,
226
        ClassMetadataBuildingContext $metadataBuildingContext
227
    )
228
    {
229 471
        parent::__construct($entityName, $metadataBuildingContext);
230
231 471
        $this->namingStrategy = $metadataBuildingContext->getNamingStrategy();
232 471
    }
233
234
    /**
235
     * @todo guilhermeblanco Remove once ClassMetadataFactory is finished
236
     *
237
     * @param string $className
238
     */
239 2
    public function setClassName(string $className)
240
    {
241 2
        $this->className = $className;
242 2
    }
243
244
    /**
245
     * @return \ArrayIterator
246
     */
247
    public function getColumnsIterator() : \ArrayIterator
248
    {
249
        $iterator = parent::getColumnsIterator();
250
251
        if ($this->discriminatorColumn) {
252
            $iterator->offsetSet($this->discriminatorColumn->getColumnName(), $this->discriminatorColumn);
253
        }
254
255
        return $iterator;
256
    }
257
258
    /**
259
     * @return \ArrayIterator
260
     */
261 11
    public function getAncestorsIterator() : \ArrayIterator
262
    {
263 11
        $ancestors = new \ArrayIterator();
264 11
        $parent    = $this;
265
266 11
        while (($parent = $parent->parent) !== null) {
267 8
            if ($parent instanceof ClassMetadata && $parent->isMappedSuperclass) {
268 1
                continue;
269
            }
270
271 7
            $ancestors->append($parent);
272
        }
273
274 11
        return $ancestors;
275
    }
276
277
    /**
278
     * @return string
279
     */
280 1261
    public function getRootClassName() : string
281
    {
282 1261
        return ($this->parent instanceof ClassMetadata && ! $this->parent->isMappedSuperclass)
283 400
            ? $this->parent->getRootClassName()
284 1261
            : $this->className
285
        ;
286
    }
287
288
    /**
289
     * Handles metadata cloning nicely.
290
     */
291 13
    public function __clone()
292
    {
293 13
        if ($this->cache) {
294 12
            $this->cache = clone $this->cache;
295
        }
296
297 13
        foreach ($this->declaredProperties as $name => $property) {
298 13
            $this->declaredProperties[$name] = clone $property;
299
        }
300 13
    }
301
302
    /**
303
     * Creates a string representation of this instance.
304
     *
305
     * @return string The string representation of this instance.
306
     *
307
     * @todo Construct meaningful string representation.
308
     */
309
    public function __toString()
310
    {
311
        return __CLASS__ . '@' . spl_object_id($this);
312
    }
313
314
    /**
315
     * Determines which fields get serialized.
316
     *
317
     * It is only serialized what is necessary for best unserialization performance.
318
     * That means any metadata properties that are not set or empty or simply have
319
     * their default value are NOT serialized.
320
     *
321
     * Parts that are also NOT serialized because they can not be properly unserialized:
322
     * - reflectionClass
323
     *
324
     * @return array The names of all the fields that should be serialized.
325
     */
326 5
    public function __sleep()
327
    {
328 5
        $serialized = [];
329
330
        // This metadata is always serialized/cached.
331 5
        $serialized = array_merge($serialized, [
332 5
            'declaredProperties',
333
            'fieldNames',
334
            //'embeddedClasses',
335
            'identifier',
336
            'className',
337
            'parent',
338
            'table',
339
            'valueGenerationPlan',
340
        ]);
341
342
        // The rest of the metadata is only serialized if necessary.
343 5
        if ($this->changeTrackingPolicy !== ChangeTrackingPolicy::DEFERRED_IMPLICIT) {
344
            $serialized[] = 'changeTrackingPolicy';
345
        }
346
347 5
        if ($this->customRepositoryClassName) {
348 1
            $serialized[] = 'customRepositoryClassName';
349
        }
350
351 5
        if ($this->inheritanceType !== InheritanceType::NONE) {
352 1
            $serialized[] = 'inheritanceType';
353 1
            $serialized[] = 'discriminatorColumn';
354 1
            $serialized[] = 'discriminatorValue';
355 1
            $serialized[] = 'discriminatorMap';
356 1
            $serialized[] = 'subClasses';
357
        }
358
359 5
        if ($this->isMappedSuperclass) {
360
            $serialized[] = 'isMappedSuperclass';
361
        }
362
363 5
        if ($this->isEmbeddedClass) {
364
            $serialized[] = 'isEmbeddedClass';
365
        }
366
367 5
        if ($this->isVersioned()) {
368
            $serialized[] = 'versionProperty';
369
        }
370
371 5
        if ($this->lifecycleCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lifecycleCallbacks of type array<string,string[]> 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...
372
            $serialized[] = 'lifecycleCallbacks';
373
        }
374
375 5
        if ($this->entityListeners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityListeners 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...
376 1
            $serialized[] = 'entityListeners';
377
        }
378
379 5
        if ($this->namedQueries) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namedQueries 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...
380 1
            $serialized[] = 'namedQueries';
381
        }
382
383 5
        if ($this->namedNativeQueries) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namedNativeQueries 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...
384
            $serialized[] = 'namedNativeQueries';
385
        }
386
387 5
        if ($this->sqlResultSetMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sqlResultSetMappings 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...
388
            $serialized[] = 'sqlResultSetMappings';
389
        }
390
391 5
        if ($this->cache) {
392
            $serialized[] = 'cache';
393
        }
394
395 5
        if ($this->readOnly) {
396 1
            $serialized[] = 'readOnly';
397
        }
398
399 5
        return $serialized;
400
    }
401
402
    /**
403
     * Restores some state that can not be serialized/unserialized.
404
     *
405
     * @param ReflectionService $reflectionService
406
     *
407
     * @return void
408
     */
409 1637
    public function wakeupReflection(ReflectionService $reflectionService) : void
410
    {
411
        // Restore ReflectionClass and properties
412 1637
        $this->reflectionClass = $reflectionService->getClass($this->className);
413
414 1637
        if (! $this->reflectionClass) {
415
            return;
416
        }
417
418 1637
        $this->className = $this->reflectionClass->getName();
419
420 1637
        foreach ($this->declaredProperties as $property) {
421
            /** @var Property $property */
422 1636
            $property->wakeupReflection($reflectionService);
423
        }
424 1637
    }
425
426
    /**
427
     * Validates Identifier.
428
     *
429
     * @return void
430
     *
431
     * @throws MappingException
432
     */
433 364
    public function validateIdentifier() : void
434
    {
435 364
        if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
436 30
            return;
437
        }
438
439
        // Verify & complete identifier mapping
440 363
        if (! $this->identifier) {
441 5
            throw MappingException::identifierRequired($this->className);
442
        }
443
444 358
        $explicitlyGeneratedProperties = array_filter($this->declaredProperties, function (Property $property) : bool {
445 358
            return $property instanceof FieldMetadata
446 358
                && $property->isPrimaryKey()
447 358
                && $property->hasValueGenerator();
448 358
        });
449
450 358
        if ($explicitlyGeneratedProperties && $this->isIdentifierComposite()) {
451
            throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->className);
452
        }
453 358
    }
454
455
    /**
456
     * Validates association targets actually exist.
457
     *
458
     * @return void
459
     *
460
     * @throws MappingException
461
     */
462 362
    public function validateAssociations() : void
463
    {
464 362
        array_map(
465 362
            function (Property $property) {
466 362
                if (! ($property instanceof AssociationMetadata)) {
467 358
                    return;
468
                }
469
470 248
                $targetEntity = $property->getTargetEntity();
471
472 248
                if (! class_exists($targetEntity)) {
473 1
                    throw MappingException::invalidTargetEntityClass($targetEntity, $this->className, $property->getName());
474
                }
475 362
            },
476 362
            $this->declaredProperties
477
        );
478 361
    }
479
480
    /**
481
     * Validates lifecycle callbacks.
482
     *
483
     * @param ReflectionService $reflectionService
484
     *
485
     * @return void
486
     *
487
     * @throws MappingException
488
     */
489 362
    public function validateLifecycleCallbacks(ReflectionService $reflectionService) : void
490
    {
491 362
        foreach ($this->lifecycleCallbacks as $callbacks) {
492
            /** @var array $callbacks */
493 11
            foreach ($callbacks as $callbackFuncName) {
494 11
                if (! $reflectionService->hasPublicMethod($this->className, $callbackFuncName)) {
495 11
                    throw MappingException::lifecycleCallbackMethodNotFound($this->className, $callbackFuncName);
496
                }
497
            }
498
        }
499 361
    }
500
501
    /**
502
     * Sets the change tracking policy used by this class.
503
     *
504
     * @param string $policy
505
     *
506
     * @return void
507
     */
508 104
    public function setChangeTrackingPolicy(string $policy) : void
509
    {
510 104
        $this->changeTrackingPolicy = $policy;
511 104
    }
512
513
    /**
514
     * Checks whether a field is part of the identifier/primary key field(s).
515
     *
516
     * @param string $fieldName The field name.
517
     *
518
     * @return bool TRUE if the field is part of the table identifier/primary key field(s), FALSE otherwise.
519
     */
520 1024
    public function isIdentifier(string $fieldName) : bool
521
    {
522 1024
        if (! $this->identifier) {
523 1
            return false;
524
        }
525
526 1023
        if (! $this->isIdentifierComposite()) {
527 1019
            return $fieldName === $this->identifier[0];
528
        }
529
530 90
        return in_array($fieldName, $this->identifier, true);
531
    }
532
533
    /**
534
     * @return bool
535
     */
536 1208
    public function isIdentifierComposite() : bool
537
    {
538 1208
        return isset($this->identifier[1]);
539
    }
540
541
    /**
542
     * Gets the named query.
543
     *
544
     * @see ClassMetadata::$namedQueries
545
     *
546
     * @param string $queryName The query name.
547
     *
548
     * @return string
549
     *
550
     * @throws MappingException
551
     */
552 4
    public function getNamedQuery($queryName) : string
553
    {
554 4
        if (! isset($this->namedQueries[$queryName])) {
555 1
            throw MappingException::queryNotFound($this->className, $queryName);
556
        }
557
558 3
        return $this->namedQueries[$queryName];
559
    }
560
561
    /**
562
     * Gets all named queries of the class.
563
     *
564
     * @return array
565
     */
566 103
    public function getNamedQueries() : array
567
    {
568 103
        return $this->namedQueries;
569
    }
570
571
    /**
572
     * Gets the named native query.
573
     *
574
     * @see ClassMetadata::$namedNativeQueries
575
     *
576
     * @param string $queryName The query name.
577
     *
578
     * @return array
579
     *
580
     * @throws MappingException
581
     */
582 15
    public function getNamedNativeQuery($queryName) : array
583
    {
584 15
        if ( ! isset($this->namedNativeQueries[$queryName])) {
585
            throw MappingException::queryNotFound($this->className, $queryName);
586
        }
587
588 15
        return $this->namedNativeQueries[$queryName];
589
    }
590
591
    /**
592
     * Gets all named native queries of the class.
593
     *
594
     * @return array
595
     */
596 3
    public function getNamedNativeQueries() : array
597
    {
598 3
        return $this->namedNativeQueries;
599
    }
600
601
    /**
602
     * Gets the result set mapping.
603
     *
604
     * @see ClassMetadata::$sqlResultSetMappings
605
     *
606
     * @param string $name The result set mapping name.
607
     *
608
     * @return array
609
     *
610
     * @throws MappingException
611
     */
612 13
    public function getSqlResultSetMapping($name)
613
    {
614 13
        if (! isset($this->sqlResultSetMappings[$name])) {
615
            throw MappingException::resultMappingNotFound($this->className, $name);
616
        }
617
618 13
        return $this->sqlResultSetMappings[$name];
619
    }
620
621
    /**
622
     * Gets all sql result set mappings of the class.
623
     *
624
     * @return array
625
     */
626 5
    public function getSqlResultSetMappings()
627
    {
628 5
        return $this->sqlResultSetMappings;
629
    }
630
631
    /**
632
     * Validates & completes the basic mapping information for field mapping.
633
     *
634
     * @param FieldMetadata $property
635
     *
636
     * @throws MappingException If something is wrong with the mapping.
637
     */
638 406
    protected function validateAndCompleteFieldMapping(FieldMetadata $property)
639
    {
640 406
        $fieldName  = $property->getName();
641 406
        $columnName = $property->getColumnName();
642
643 406
        if (empty($columnName)) {
644 349
            $columnName = $this->namingStrategy->propertyToColumnName($fieldName, $this->className);
645
646 349
            $property->setColumnName($columnName);
647
        }
648
649 406
        if (! $this->isMappedSuperclass) {
650 399
            $property->setTableName($this->getTableName());
651
        }
652
653
        // Check for already declared column
654 406
        if (isset($this->fieldNames[$columnName]) ||
655 406
            ($this->discriminatorColumn !== null && $this->discriminatorColumn->getColumnName() === $columnName)) {
656 2
            throw MappingException::duplicateColumnName($this->className, $columnName);
657
        }
658
659
        // Complete id mapping
660 405
        if ($property->isPrimaryKey()) {
661 389
            if ($this->versionProperty !== null && $this->versionProperty->getName() === $fieldName) {
662
                throw MappingException::cannotVersionIdField($this->className, $fieldName);
663
            }
664
665 389
            if ($property->getType()->canRequireSQLConversion()) {
666
                throw MappingException::sqlConversionNotAllowedForPrimaryKeyProperties($property);
667
            }
668
669 389
            if (! in_array($fieldName, $this->identifier)) {
670 389
                $this->identifier[] = $fieldName;
671
            }
672
        }
673
674 405
        $this->fieldNames[$columnName] = $fieldName;
675 405
    }
676
677
    /**
678
     * Validates & completes the basic mapping information for field mapping.
679
     *
680
     * @param VersionFieldMetadata $property
681
     *
682
     * @throws MappingException If something is wrong with the mapping.
683
     */
684 21
    protected function validateAndCompleteVersionFieldMapping(VersionFieldMetadata $property)
685
    {
686 21
        $this->versionProperty = $property;
687
688 21
        $options = $property->getOptions();
689
690 21
        if (isset($options['default'])) {
691
            return;
692
        }
693
694 21
        if (in_array($property->getTypeName(), ['integer', 'bigint', 'smallint'])) {
695 20
            $property->setOptions(array_merge($options, ['default' => 1]));
696
697 20
            return;
698
        }
699
700 2
        if ($property->getTypeName() === 'datetime') {
701 1
            $property->setOptions(array_merge($options, ['default' => 'CURRENT_TIMESTAMP']));
702
703 1
            return;
704
        }
705
706 1
        throw MappingException::unsupportedOptimisticLockingType($property->getType());
707
    }
708
709
    /**
710
     * Validates & completes the basic mapping information that is common to all
711
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
712
     *
713
     * @param AssociationMetadata $property
714
     *
715
     * @throws MappingException If something is wrong with the mapping.
716
     * @throws CacheException   If entity is not cacheable.
717
     */
718 283
    protected function validateAndCompleteAssociationMapping(AssociationMetadata $property)
719
    {
720 283
        $fieldName    = $property->getName();
721 283
        $targetEntity = $property->getTargetEntity();
722
723 283
        if (! $targetEntity) {
724
            throw MappingException::missingTargetEntity($fieldName);
725
        }
726
727 283
        $property->setSourceEntity($this->className);
728 283
        $property->setOwningSide($property->getMappedBy() === null);
729 283
        $property->setTargetEntity($targetEntity);
730
731
        // Complete id mapping
732 283
        if ($property->isPrimaryKey()) {
733 44
            if ($property->isOrphanRemoval()) {
734 1
                throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->className, $fieldName);
735
            }
736
737 43
            if ( ! in_array($property->getName(), $this->identifier)) {
738 43
                if ($property instanceof ToOneAssociationMetadata && count($property->getJoinColumns()) >= 2) {
739
                    throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
740
                        $property->getTargetEntity(),
741
                        $this->className,
742
                        $fieldName
743
                    );
744
                }
745
746 43
                $this->identifier[] = $property->getName();
747
            }
748
749 43
            if ($this->cache && !$property->getCache()) {
750 2
                throw CacheException::nonCacheableEntityAssociation($this->className, $fieldName);
751
            }
752
753 41
            if ($property instanceof ToManyAssociationMetadata) {
754 1
                throw MappingException::illegalToManyIdentifierAssociation($this->className, $property->getName());
755
            }
756
        }
757
758
        // Cascades
759 279
        $cascadeTypes = ['remove', 'persist', 'refresh'];
760 279
        $cascades     = array_map('strtolower', $property->getCascade());
761
762 279
        if (in_array('all', $cascades)) {
763 4
            $cascades = $cascadeTypes;
764
        }
765
766 279
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
767 1
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
768
769 1
            throw MappingException::invalidCascadeOption($diffCascades, $this->className, $fieldName);
770
        }
771
772 278
        $property->setCascade($cascades);
773 278
    }
774
775
    /**
776
     * Validates & completes a to-one association mapping.
777
     *
778
     * @param ToOneAssociationMetadata $property The association mapping to validate & complete.
779
     *
780
     * @throws \RuntimeException
781
     * @throws MappingException
782
     */
783 243
    protected function validateAndCompleteToOneAssociationMetadata(ToOneAssociationMetadata $property)
784
    {
785 243
        $fieldName = $property->getName();
786
787 243
        if ($property->getJoinColumns()) {
788 171
            $property->setOwningSide(true);
789
        }
790
791 243
        if ($property->isOwningSide()) {
792 240
            if (empty($property->getJoinColumns())) {
793
                // Apply default join column
794 82
                $property->addJoinColumn(new JoinColumnMetadata());
795
            }
796
797 240
            $uniqueConstraintColumns = [];
798
799 240
            foreach ($property->getJoinColumns() as $joinColumn) {
800
                /** @var JoinColumnMetadata $joinColumn */
801 240
                if ($property instanceof OneToOneAssociationMetadata && $this->inheritanceType !== InheritanceType::SINGLE_TABLE) {
802 118
                    if (1 === count($property->getJoinColumns())) {
803 116
                        if (! $property->isPrimaryKey()) {
804 116
                            $joinColumn->setUnique(true);
805
                        }
806
                    } else {
807 2
                        $uniqueConstraintColumns[] = $joinColumn->getColumnName();
808
                    }
809
                }
810
811 240
                $joinColumn->setTableName(! $this->isMappedSuperclass ? $this->getTableName() : null);
812
813 240
                if (! $joinColumn->getColumnName()) {
814 101
                    $joinColumn->setColumnName($this->namingStrategy->joinColumnName($fieldName, $this->className));
815
                }
816
817 240
                if (! $joinColumn->getReferencedColumnName()) {
818 82
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
819
                }
820
821 240
                $this->fieldNames[$joinColumn->getColumnName()] = $fieldName;
822
            }
823
824 240
            if ($uniqueConstraintColumns) {
825 2
                if ( ! $this->table) {
826
                    throw new \RuntimeException(
827
                        "ClassMetadata::setTable() has to be called before defining a one to one relationship."
828
                    );
829
                }
830
831 2
                $this->table->addUniqueConstraint(
832
                    [
833 2
                        'name'    => sprintf('%s_uniq', $fieldName),
834 2
                        'columns' => $uniqueConstraintColumns,
835
                        'options' => [],
836
                        'flags'   => [],
837
                    ]
838
                );
839
            }
840
        }
841
842 243
        if ($property->isOrphanRemoval()) {
843 9
            $cascades = $property->getCascade();
844
845 9
            if (! in_array('remove', $cascades)) {
846 8
                $cascades[] = 'remove';
847
848 8
                $property->setCascade($cascades);
849
            }
850
851
            // @todo guilhermeblanco where is this used?
852
            // @todo guilhermeblanco Shouldn￿'t we iterate through JoinColumns to set non-uniqueness?
853
            //$property->setUnique(false);
854
        }
855
856 243
        if ($property->isPrimaryKey() && ! $property->isOwningSide()) {
857 1
            throw MappingException::illegalInverseIdentifierAssociation($this->className, $fieldName);
858
        }
859 242
    }
860
861
    /**
862
     * Validates & completes a to-many association mapping.
863
     *
864
     * @param ToManyAssociationMetadata $property The association mapping to validate & complete.
865
     *
866
     * @throws MappingException
867
     */
868 167
    protected function validateAndCompleteToManyAssociationMetadata(ToManyAssociationMetadata $property)
869
    {
870
        // Do nothing
871 167
    }
872
873
    /**
874
     * Validates & completes a one-to-one association mapping.
875
     *
876
     * @param OneToOneAssociationMetadata $property The association mapping to validate & complete.
877
     */
878 125
    protected function validateAndCompleteOneToOneMapping(OneToOneAssociationMetadata $property)
879
    {
880
        // Do nothing
881 125
    }
882
883
    /**
884
     * Validates & completes a many-to-one association mapping.
885
     *
886
     * @param ManyToOneAssociationMetadata $property The association mapping to validate & complete.
887
     *
888
     * @throws MappingException
889
     */
890 141
    protected function validateAndCompleteManyToOneMapping(ManyToOneAssociationMetadata $property)
891
    {
892
        // A many-to-one mapping is essentially a one-one backreference
893 141
        if ($property->isOrphanRemoval()) {
894
            throw MappingException::illegalOrphanRemoval($this->className, $property->getName());
895
        }
896 141
    }
897
898
    /**
899
     * Validates & completes a one-to-many association mapping.
900
     *
901
     * @param OneToManyAssociationMetadata $property The association mapping to validate & complete.
902
     *
903
     * @throws MappingException
904
     */
905 114
    protected function validateAndCompleteOneToManyMapping(OneToManyAssociationMetadata $property)
906
    {
907
        // OneToMany MUST be inverse side
908 114
        $property->setOwningSide(false);
909
910
        // OneToMany MUST have mappedBy
911 114
        if (! $property->getMappedBy()) {
912
            throw MappingException::oneToManyRequiresMappedBy($property->getName());
913
        }
914
915 114
        if ($property->isOrphanRemoval()) {
916 23
            $cascades = $property->getCascade();
917
918 23
            if (! in_array('remove', $cascades)) {
919 20
                $cascades[] = 'remove';
920
921 20
                $property->setCascade($cascades);
922
            }
923
        }
924 114
    }
925
926
    /**
927
     * Validates & completes a many-to-many association mapping.
928
     *
929
     * @param ManyToManyAssociationMetadata $property The association mapping to validate & complete.
930
     *
931
     * @throws MappingException
932
     */
933 105
    protected function validateAndCompleteManyToManyMapping(ManyToManyAssociationMetadata $property)
934
    {
935 105
        if ($property->isOwningSide()) {
936
            // owning side MUST have a join table
937 93
            $joinTable = $property->getJoinTable() ?: new JoinTableMetadata();
938
939 93
            $property->setJoinTable($joinTable);
940
941 93
            if (! $joinTable->getName()) {
942 18
                $joinTableName = $this->namingStrategy->joinTableName(
943 18
                    $property->getSourceEntity(),
944 18
                    $property->getTargetEntity(),
945 18
                    $property->getName()
946
                );
947
948 18
                $joinTable->setName($joinTableName);
949
            }
950
951 93
            $selfReferencingEntityWithoutJoinColumns = $property->getSourceEntity() == $property->getTargetEntity() && ! $joinTable->hasColumns();
952
953 93
            if (! $joinTable->getJoinColumns()) {
954 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
955 16
                $sourceReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'source' : $referencedColumnName;
956 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getSourceEntity(), $sourceReferenceName);
957 16
                $joinColumn           = new JoinColumnMetadata();
958
959 16
                $joinColumn->setColumnName($columnName);
960 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
961 16
                $joinColumn->setOnDelete('CASCADE');
962
963 16
                $joinTable->addJoinColumn($joinColumn);
964
            }
965
966 93
            if (! $joinTable->getInverseJoinColumns()) {
967 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
968 16
                $targetReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'target' : $referencedColumnName;
969 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getTargetEntity(), $targetReferenceName);
970 16
                $joinColumn           = new JoinColumnMetadata();
971
972 16
                $joinColumn->setColumnName($columnName);
973 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
974 16
                $joinColumn->setOnDelete('CASCADE');
975
976 16
                $joinTable->addInverseJoinColumn($joinColumn);
977
            }
978
979 93
            foreach ($joinTable->getJoinColumns() as $joinColumn) {
980
                /** @var JoinColumnMetadata $joinColumn */
981 93
                if (! $joinColumn->getReferencedColumnName()) {
982 2
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
983
                }
984
985 93
                $referencedColumnName = $joinColumn->getReferencedColumnName();
986
987 93
                if (! $joinColumn->getColumnName()) {
988 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
989 2
                        $property->getSourceEntity(),
990 2
                        $referencedColumnName
991
                    );
992
993 93
                    $joinColumn->setColumnName($columnName);
994
                }
995
            }
996
997 93
            foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) {
998
                /** @var JoinColumnMetadata $inverseJoinColumn */
999 93
                if (! $inverseJoinColumn->getReferencedColumnName()) {
1000 2
                    $inverseJoinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
1001
                }
1002
1003 93
                $referencedColumnName = $inverseJoinColumn->getReferencedColumnName();
1004
1005 93
                if (! $inverseJoinColumn->getColumnName()) {
1006 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
1007 2
                        $property->getTargetEntity(),
1008 2
                        $referencedColumnName
1009
                    );
1010
1011 93
                    $inverseJoinColumn->setColumnName($columnName);
1012
                }
1013
            }
1014
        }
1015 105
    }
1016
1017
    /**
1018
     * {@inheritDoc}
1019
     */
1020 397
    public function getIdentifierFieldNames()
1021
    {
1022 397
        return $this->identifier;
1023
    }
1024
1025
    /**
1026
     * Gets the name of the single id field. Note that this only works on
1027
     * entity classes that have a single-field pk.
1028
     *
1029
     * @return string
1030
     *
1031
     * @throws MappingException If the class has a composite primary key.
1032
     */
1033 149
    public function getSingleIdentifierFieldName()
1034
    {
1035 149
        if ($this->isIdentifierComposite()) {
1036 1
            throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->className);
1037
        }
1038
1039 148
        if ( ! isset($this->identifier[0])) {
1040 1
            throw MappingException::noIdDefined($this->className);
1041
        }
1042
1043 147
        return $this->identifier[0];
1044
    }
1045
1046
    /**
1047
     * INTERNAL:
1048
     * Sets the mapped identifier/primary key fields of this class.
1049
     * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
1050
     *
1051
     * @param array $identifier
1052
     *
1053
     * @return void
1054
     */
1055 101
    public function setIdentifier(array $identifier)
1056
    {
1057 101
        $this->identifier = $identifier;
1058 101
    }
1059
1060
    /**
1061
     * {@inheritDoc}
1062
     */
1063 1049
    public function getIdentifier()
1064
    {
1065 1049
        return $this->identifier;
1066
    }
1067
1068
    /**
1069
     * {@inheritDoc}
1070
     */
1071 186
    public function hasField($fieldName)
1072
    {
1073 186
        return isset($this->declaredProperties[$fieldName])
1074 186
            && $this->declaredProperties[$fieldName] instanceof FieldMetadata;
1075
    }
1076
1077
    /**
1078
     * Returns an array with identifier column names and their corresponding ColumnMetadata.
1079
     *
1080
     * @param EntityManagerInterface $em
1081
     *
1082
     * @return array
1083
     */
1084 434
    public function getIdentifierColumns(EntityManagerInterface $em) : array
1085
    {
1086 434
        $columns = [];
1087
1088 434
        foreach ($this->identifier as $idProperty) {
1089 434
            $property = $this->getProperty($idProperty);
1090
1091 434
            if ($property instanceof FieldMetadata) {
1092 429
                $columns[$property->getColumnName()] = $property;
1093
1094 429
                continue;
1095
            }
1096
1097
            /** @var AssociationMetadata $property */
1098
1099
            // Association defined as Id field
1100 24
            $targetClass = $em->getClassMetadata($property->getTargetEntity());
1101
1102 24
            if (! $property->isOwningSide()) {
1103
                $property    = $targetClass->getProperty($property->getMappedBy());
1104
                $targetClass = $em->getClassMetadata($property->getTargetEntity());
1105
            }
1106
1107 24
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
1108
                ? $property->getJoinTable()->getInverseJoinColumns()
1109 24
                : $property->getJoinColumns()
1110
            ;
1111
1112 24
            foreach ($joinColumns as $joinColumn) {
1113
                /** @var JoinColumnMetadata $joinColumn */
1114 24
                $columnName           = $joinColumn->getColumnName();
1115 24
                $referencedColumnName = $joinColumn->getReferencedColumnName();
1116
1117 24
                if (! $joinColumn->getType()) {
1118 12
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $em));
1119
                }
1120
1121 24
                $columns[$columnName] = $joinColumn;
1122
            }
1123
        }
1124
1125 434
        return $columns;
1126
    }
1127
1128
    /**
1129
     * Gets the name of the primary table.
1130
     *
1131
     * @return string|null
1132
     */
1133 1585
    public function getTableName() : ?string
1134
    {
1135 1585
        return $this->table->getName();
1136
    }
1137
1138
    /**
1139
     * Gets primary table's schema name.
1140
     *
1141
     * @return string|null
1142
     */
1143 14
    public function getSchemaName() : ?string
1144
    {
1145 14
        return $this->table->getSchema();
1146
    }
1147
1148
    /**
1149
     * Gets the table name to use for temporary identifier tables of this class.
1150
     *
1151
     * @return string
1152
     */
1153 7
    public function getTemporaryIdTableName() : string
1154
    {
1155 7
        $schema = null === $this->getSchemaName()
1156 6
            ? ''
1157 7
            : $this->getSchemaName() . '_'
1158
        ;
1159
1160
        // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
1161 7
        return $schema . $this->getTableName() . '_id_tmp';
1162
    }
1163
1164
    /**
1165
     * Sets the mapped subclasses of this class.
1166
     *
1167
     * @todo guilhermeblanco Only used for ClassMetadataTest. Remove if possible!
1168
     *
1169
     * @param array $subclasses The names of all mapped subclasses.
1170
     *
1171
     * @return void
1172
     */
1173 4
    public function setSubclasses(array $subclasses) : void
1174
    {
1175 4
        foreach ($subclasses as $subclass) {
1176 3
            $this->subClasses[] = $subclass;
1177
        }
1178 4
    }
1179
1180
    /**
1181
     * @return array
1182
     */
1183 1075
    public function getSubClasses() : array
1184
    {
1185 1075
        return $this->subClasses;
1186
    }
1187
1188
    /**
1189
     * Sets the inheritance type used by the class and its subclasses.
1190
     *
1191
     * @param integer $type
1192
     *
1193
     * @return void
1194
     *
1195
     * @throws MappingException
1196
     */
1197 119
    public function setInheritanceType($type) : void
1198
    {
1199 119
        if ( ! $this->isInheritanceType($type)) {
1200
            throw MappingException::invalidInheritanceType($this->className, $type);
1201
        }
1202
1203 119
        $this->inheritanceType = $type;
1204 119
    }
1205
1206
    /**
1207
     * Sets the override property mapping for an entity relationship.
1208
     *
1209
     * @param Property $property
1210
     *
1211
     * @return void
1212
     *
1213
     * @throws \RuntimeException
1214
     * @throws MappingException
1215
     * @throws CacheException
1216
     */
1217 12
    public function setPropertyOverride(Property $property) : void
1218
    {
1219 12
        $fieldName = $property->getName();
1220
1221 12
        if (! isset($this->declaredProperties[$fieldName])) {
1222 2
            throw MappingException::invalidOverrideFieldName($this->className, $fieldName);
1223
        }
1224
1225 10
        $originalProperty          = $this->getProperty($fieldName);
1226 10
        $originalPropertyClassName = get_class($originalProperty);
1227
1228
        // If moving from transient to persistent, assume it's a new property
1229 10
        if ($originalPropertyClassName === TransientMetadata::class) {
1230 1
            unset($this->declaredProperties[$fieldName]);
1231
1232 1
            $this->addProperty($property);
1233
1234 1
            return;
1235
        }
1236
1237
        // Do not allow to change property type
1238 9
        if ($originalPropertyClassName !== get_class($property)) {
1239
            throw MappingException::invalidOverridePropertyType($this->className, $fieldName);
1240
        }
1241
1242
        // Do not allow to change version property
1243 9
        if ($originalProperty instanceof VersionFieldMetadata) {
1244
            throw MappingException::invalidOverrideVersionField($this->className, $fieldName);
1245
        }
1246
1247 9
        unset($this->declaredProperties[$fieldName]);
1248
1249 9
        if ($property instanceof FieldMetadata) {
1250
            // Unset defined fieldName prior to override
1251 5
            unset($this->fieldNames[$originalProperty->getColumnName()]);
1252
1253
            // Revert what should not be allowed to change
1254 5
            $property->setDeclaringClass($originalProperty->getDeclaringClass());
1255 5
            $property->setPrimaryKey($originalProperty->isPrimaryKey());
1256 9
        } elseif ($property instanceof AssociationMetadata) {
1257
            // Unset all defined fieldNames prior to override
1258 9
            if ($originalProperty instanceof ToOneAssociationMetadata && $originalProperty->isOwningSide()) {
1259 5
                foreach ($originalProperty->getJoinColumns() as $joinColumn) {
1260 5
                    unset($this->fieldNames[$joinColumn->getColumnName()]);
1261
                }
1262
            }
1263
1264
            // Override what it should be allowed to change
1265 9
            if ($property->getInversedBy()) {
1266 2
                $originalProperty->setInversedBy($property->getInversedBy());
1267
            }
1268
1269 9
            if ($property->getFetchMode() !== $originalProperty->getFetchMode()) {
1270 2
                $originalProperty->setFetchMode($property->getFetchMode());
1271
            }
1272
1273 9
            if ($originalProperty instanceof ToOneAssociationMetadata && $property->getJoinColumns()) {
1274 5
                $originalProperty->setJoinColumns($property->getJoinColumns());
1275 8
            } elseif ($originalProperty instanceof ManyToManyAssociationMetadata && $property->getJoinTable()) {
1276 4
                $originalProperty->setJoinTable($property->getJoinTable());
1277
            }
1278
1279 9
            $property = $originalProperty;
1280
        }
1281
1282 9
        $this->addProperty($property);
1283 9
    }
1284
1285
    /**
1286
     * Checks if this entity is the root in any entity-inheritance-hierarchy.
1287
     *
1288
     * @return bool
1289
     */
1290 334
    public function isRootEntity()
1291
    {
1292 334
        return $this->className === $this->getRootClassName();
1293
    }
1294
1295
    /**
1296
     * Checks whether a mapped field is inherited from a superclass.
1297
     *
1298
     * @param string $fieldName
1299
     *
1300
     * @return boolean TRUE if the field is inherited, FALSE otherwise.
1301
     */
1302 616
    public function isInheritedProperty($fieldName)
1303
    {
1304 616
        $declaringClass = $this->declaredProperties[$fieldName]->getDeclaringClass();
1305
1306 616
        return ! ($declaringClass->className === $this->className);
1307
    }
1308
1309
    /**
1310
     * {@inheritdoc}
1311
     */
1312 451
    public function setTable(TableMetadata $table) : void
1313
    {
1314 451
        $this->table = $table;
1315
1316 451
        if (empty($table->getName())) {
1317
            $table->setName($this->namingStrategy->classToTableName($this->className));
1318
        }
1319 451
    }
1320
1321
    /**
1322
     * Checks whether the given type identifies an inheritance type.
1323
     *
1324
     * @param integer $type
1325
     *
1326
     * @return boolean TRUE if the given type identifies an inheritance type, FALSe otherwise.
1327
     */
1328 119
    private function isInheritanceType($type)
1329
    {
1330 119
        return $type == InheritanceType::NONE
1331 92
            || $type == InheritanceType::SINGLE_TABLE
1332 52
            || $type == InheritanceType::JOINED
1333 119
            || $type == InheritanceType::TABLE_PER_CLASS;
1334
    }
1335
1336
    /**
1337
     * @param string $columnName
1338
     *
1339
     * @return LocalColumnMetadata|null
1340
     */
1341 911
    public function getColumn(string $columnName): ?LocalColumnMetadata
1342
    {
1343 911
        foreach ($this->declaredProperties as $property) {
1344 911
            if ($property instanceof LocalColumnMetadata && $property->getColumnName() === $columnName) {
1345 911
                return $property;
1346
            }
1347
        }
1348
1349
        return null;
1350
    }
1351
1352
    /**
1353
     * Add a property mapping.
1354
     *
1355
     * @param Property $property
1356
     *
1357
     * @throws \RuntimeException
1358
     * @throws MappingException
1359
     * @throws CacheException
1360
     */
1361 429
    public function addProperty(Property $property)
1362
    {
1363 429
        $fieldName = $property->getName();
1364
1365
        // Check for empty field name
1366 429
        if (empty($fieldName)) {
1367 1
            throw MappingException::missingFieldName($this->className);
1368
        }
1369
1370 428
        $property->setDeclaringClass($this);
1371
1372
        switch (true) {
1373 428
            case ($property instanceof VersionFieldMetadata):
1374 21
                $this->validateAndCompleteFieldMapping($property);
1375 21
                $this->validateAndCompleteVersionFieldMapping($property);
1376 20
                break;
1377
1378 427
            case ($property instanceof FieldMetadata):
1379 405
                $this->validateAndCompleteFieldMapping($property);
1380 404
                break;
1381
1382 286
            case ($property instanceof OneToOneAssociationMetadata):
1383 127
                $this->validateAndCompleteAssociationMapping($property);
1384 126
                $this->validateAndCompleteToOneAssociationMetadata($property);
1385 125
                $this->validateAndCompleteOneToOneMapping($property);
1386 125
                break;
1387
1388 223
            case ($property instanceof OneToManyAssociationMetadata):
1389 114
                $this->validateAndCompleteAssociationMapping($property);
1390 114
                $this->validateAndCompleteToManyAssociationMetadata($property);
1391 114
                $this->validateAndCompleteOneToManyMapping($property);
1392 114
                break;
1393
1394 219
            case ($property instanceof ManyToOneAssociationMetadata):
1395 144
                $this->validateAndCompleteAssociationMapping($property);
1396 141
                $this->validateAndCompleteToOneAssociationMetadata($property);
1397 141
                $this->validateAndCompleteManyToOneMapping($property);
1398 141
                break;
1399
1400 123
            case ($property instanceof ManyToManyAssociationMetadata):
1401 106
                $this->validateAndCompleteAssociationMapping($property);
1402 105
                $this->validateAndCompleteToManyAssociationMetadata($property);
1403 105
                $this->validateAndCompleteManyToManyMapping($property);
1404 105
                break;
1405
1406
            default:
1407
                // Transient properties are ignored on purpose here! =)
1408 32
                break;
1409
        }
1410
1411 420
        $this->addDeclaredProperty($property);
1412 420
    }
1413
1414
    /**
1415
     * INTERNAL:
1416
     * Adds a property mapping without completing/validating it.
1417
     * This is mainly used to add inherited property mappings to derived classes.
1418
     *
1419
     * @param Property $property
1420
     *
1421
     * @return void
1422
     */
1423 99
    public function addInheritedProperty(Property $property)
1424
    {
1425 99
        $inheritedProperty = clone $property;
1426 99
        $declaringClass    = $property->getDeclaringClass();
1427
1428 99
        if ($inheritedProperty instanceof FieldMetadata) {
1429 98
            if (! $declaringClass->isMappedSuperclass) {
1430 75
                $inheritedProperty->setTableName($property->getTableName());
1431
            }
1432
1433 98
            $this->fieldNames[$property->getColumnName()] = $property->getName();
1434 43
        } elseif ($inheritedProperty instanceof AssociationMetadata) {
1435 42
            if ($declaringClass->isMappedSuperclass) {
1436 10
                $inheritedProperty->setSourceEntity($this->className);
1437
            }
1438
1439
            // Need to add inherited fieldNames
1440 42
            if ($inheritedProperty instanceof ToOneAssociationMetadata && $inheritedProperty->isOwningSide()) {
1441 35
                foreach ($inheritedProperty->getJoinColumns() as $joinColumn) {
1442
                    /** @var JoinColumnMetadata $joinColumn */
1443 34
                    $this->fieldNames[$joinColumn->getColumnName()] = $property->getName();
1444
                }
1445
            }
1446
        }
1447
1448 99
        if (isset($this->declaredProperties[$property->getName()])) {
1449 1
            throw MappingException::duplicateProperty($this->className, $this->getProperty($property->getName()));
1450
        }
1451
1452 99
        $this->declaredProperties[$property->getName()] = $inheritedProperty;
1453
1454 99
        if ($inheritedProperty instanceof VersionFieldMetadata) {
1455 4
            $this->versionProperty = $inheritedProperty;
1456
        }
1457 99
    }
1458
1459
    /**
1460
     * INTERNAL:
1461
     * Adds a named query to this class.
1462
     *
1463
     * @param string $name
1464
     * @param string $query
1465
     *
1466
     * @return void
1467
     *
1468
     * @throws MappingException
1469
     */
1470 20
    public function addNamedQuery(string $name, string $query)
1471
    {
1472 20
        if (isset($this->namedQueries[$name])) {
1473 1
            throw MappingException::duplicateQueryMapping($this->className, $name);
1474
        }
1475
1476 20
        $this->namedQueries[$name] = $query;
1477 20
    }
1478
1479
    /**
1480
     * INTERNAL:
1481
     * Adds a named native query to this class.
1482
     *
1483
     * @param string $name
1484
     * @param string $query
1485
     * @param array  $queryMapping
1486
     *
1487
     * @return void
1488
     *
1489
     * @throws MappingException
1490
     */
1491 25
    public function addNamedNativeQuery(string $name, string $query, array $queryMapping)
1492
    {
1493 25
        if (isset($this->namedNativeQueries[$name])) {
1494 1
            throw MappingException::duplicateQueryMapping($this->className, $name);
1495
        }
1496
1497 25
        if (! isset($queryMapping['resultClass']) && ! isset($queryMapping['resultSetMapping'])) {
1498
            throw MappingException::missingQueryMapping($this->className, $name);
1499
        }
1500
1501 25
        $this->namedNativeQueries[$name] = array_merge(['query' => $query], $queryMapping);
1502 25
    }
1503
1504
    /**
1505
     * INTERNAL:
1506
     * Adds a sql result set mapping to this class.
1507
     *
1508
     * @param array $resultMapping
1509
     *
1510
     * @return void
1511
     *
1512
     * @throws MappingException
1513
     */
1514 25
    public function addSqlResultSetMapping(array $resultMapping)
1515
    {
1516 25
        if (!isset($resultMapping['name'])) {
1517
            throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->className);
1518
        }
1519
1520 25
        if (isset($this->sqlResultSetMappings[$resultMapping['name']])) {
1521 1
            throw MappingException::duplicateResultSetMapping($this->className, $resultMapping['name']);
1522
        }
1523
1524 25
        if (isset($resultMapping['entities'])) {
1525 25
            foreach ($resultMapping['entities'] as $key => $entityResult) {
1526 25
                if (! isset($entityResult['entityClass'])) {
1527 1
                    throw MappingException::missingResultSetMappingEntity($this->className, $resultMapping['name']);
1528
                }
1529
1530 24
                $entityClassName                                = $entityResult['entityClass'];
1531 24
                $resultMapping['entities'][$key]['entityClass'] = $entityClassName;
1532
1533 24
                if (isset($entityResult['fields'])) {
1534 20
                    foreach ($entityResult['fields'] as $k => $field) {
1535 20
                        if (! isset($field['name'])) {
1536
                            throw MappingException::missingResultSetMappingFieldName($this->className, $resultMapping['name']);
1537
                        }
1538
1539 20
                        if (! isset($field['column'])) {
1540 12
                            $fieldName = $field['name'];
1541
1542 12
                            if (strpos($fieldName, '.')) {
1543 6
                                list(, $fieldName) = explode('.', $fieldName);
1544
                            }
1545
1546 24
                            $resultMapping['entities'][$key]['fields'][$k]['column'] = $fieldName;
1547
                        }
1548
                    }
1549
                }
1550
            }
1551
        }
1552
1553 24
        $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping;
1554 24
    }
1555
1556
    /**
1557
     * Registers a custom repository class for the entity class.
1558
     *
1559
     * @param string|null $repositoryClassName The class name of the custom mapper.
1560
     *
1561
     * @return void
1562
     */
1563 32
    public function setCustomRepositoryClassName(?string $repositoryClassName)
1564
    {
1565 32
        $this->customRepositoryClassName = $repositoryClassName;
1566 32
    }
1567
1568
    /**
1569
     * @return string|null
1570
     */
1571 180
    public function getCustomRepositoryClassName() : ?string
1572
    {
1573 180
        return $this->customRepositoryClassName;
1574
    }
1575
1576
    /**
1577
     * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
1578
     *
1579
     * @param string $lifecycleEvent
1580
     *
1581
     * @return boolean
1582
     */
1583
    public function hasLifecycleCallbacks($lifecycleEvent)
1584
    {
1585
        return isset($this->lifecycleCallbacks[$lifecycleEvent]);
1586
    }
1587
1588
    /**
1589
     * Gets the registered lifecycle callbacks for an event.
1590
     *
1591
     * @param string $event
1592
     *
1593
     * @return array
1594
     */
1595
    public function getLifecycleCallbacks($event)
1596
    {
1597
        return $this->lifecycleCallbacks[$event] ?? [];
1598
    }
1599
1600
    /**
1601
     * Adds a lifecycle callback for entities of this class.
1602
     *
1603
     * @param string $callback
1604
     * @param string $event
1605
     *
1606
     * @return void
1607
     */
1608 19
    public function addLifecycleCallback($callback, $event)
1609
    {
1610 19
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
1611 3
            return;
1612
        }
1613
1614 19
        $this->lifecycleCallbacks[$event][] = $callback;
1615 19
    }
1616
1617
    /**
1618
     * Sets the lifecycle callbacks for entities of this class.
1619
     * Any previously registered callbacks are overwritten.
1620
     *
1621
     * @param array $callbacks
1622
     *
1623
     * @return void
1624
     */
1625 99
    public function setLifecycleCallbacks(array $callbacks) : void
1626
    {
1627 99
        $this->lifecycleCallbacks = $callbacks;
1628 99
    }
1629
1630
    /**
1631
     * Adds a entity listener for entities of this class.
1632
     *
1633
     * @param string $eventName The entity lifecycle event.
1634
     * @param string $class     The listener class.
1635
     * @param string $method    The listener callback method.
1636
     *
1637
     * @return void
1638
     *
1639
     * @throws \Doctrine\ORM\Mapping\MappingException
1640
     */
1641 19
    public function addEntityListener(string $eventName, string $class, string $method) : void
1642
    {
1643
        $listener = [
1644 19
            'class'  => $class,
1645 19
            'method' => $method,
1646
        ];
1647
1648 19
        if (! class_exists($class)) {
1649 1
            throw MappingException::entityListenerClassNotFound($class, $this->className);
1650
        }
1651
1652 18
        if (! method_exists($class, $method)) {
1653 1
            throw MappingException::entityListenerMethodNotFound($class, $method, $this->className);
1654
        }
1655
1656 17
        if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
1657 1
            throw MappingException::duplicateEntityListener($class, $method, $this->className);
1658
        }
1659
1660 17
        $this->entityListeners[$eventName][] = $listener;
1661 17
    }
1662
1663
    /**
1664
     * Sets the discriminator column definition.
1665
     *
1666
     * @param DiscriminatorColumnMetadata $discriminatorColumn
1667
     *
1668
     * @return void
1669
     *
1670
     * @throws MappingException
1671
     *
1672
     * @see getDiscriminatorColumn()
1673
     */
1674 94
    public function setDiscriminatorColumn(DiscriminatorColumnMetadata $discriminatorColumn) : void
1675
    {
1676 94
        if (isset($this->fieldNames[$discriminatorColumn->getColumnName()])) {
1677 1
            throw MappingException::duplicateColumnName($this->className, $discriminatorColumn->getColumnName());
1678
        }
1679
1680 93
        $discriminatorColumn->setTableName($discriminatorColumn->getTableName() ?? $this->getTableName());
1681
1682 93
        $allowedTypeList = ['boolean', 'array', 'object', 'datetime', 'time', 'date'];
1683
1684 93
        if (in_array($discriminatorColumn->getTypeName(), $allowedTypeList, true)) {
1685
            throw MappingException::invalidDiscriminatorColumnType($discriminatorColumn->getTypeName());
1686
        }
1687
1688 93
        $this->discriminatorColumn = $discriminatorColumn;
1689 93
    }
1690
1691
    /**
1692
     * Sets the discriminator values used by this class.
1693
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
1694
     *
1695
     * @param array $map
1696
     *
1697
     * @return void
1698
     *
1699
     * @throws MappingException
1700
     */
1701 91
    public function setDiscriminatorMap(array $map) : void
1702
    {
1703 91
        foreach ($map as $value => $className) {
1704 91
            $this->addDiscriminatorMapClass($value, $className);
1705
        }
1706 91
    }
1707
1708
    /**
1709
     * Adds one entry of the discriminator map with a new class and corresponding name.
1710
     *
1711
     * @param string|int $name
1712
     * @param string     $className
1713
     *
1714
     * @return void
1715
     *
1716
     * @throws MappingException
1717
     */
1718 91
    public function addDiscriminatorMapClass($name, string $className) : void
1719
    {
1720 91
        $this->discriminatorMap[$name] = $className;
1721
1722 91
        if ($this->className === $className) {
1723 77
            $this->discriminatorValue = $name;
1724
1725 77
            return;
1726
        }
1727
1728 90
        if (! (class_exists($className) || interface_exists($className))) {
1729
            throw MappingException::invalidClassInDiscriminatorMap($className, $this->className);
1730
        }
1731
1732 90
        if (is_subclass_of($className, $this->className) && ! in_array($className, $this->subClasses, true)) {
1733 85
            $this->subClasses[] = $className;
1734
        }
1735 90
    }
1736
1737
    /**
1738
     * @return ValueGenerationPlan
1739
     */
1740 1028
    public function getValueGenerationPlan() : ValueGenerationPlan
1741
    {
1742 1028
        return $this->valueGenerationPlan;
1743
    }
1744
1745
    /**
1746
     * @param ValueGenerationPlan $valueGenerationPlan
1747
     */
1748 364
    public function setValueGenerationPlan(ValueGenerationPlan $valueGenerationPlan) : void
1749
    {
1750 364
        $this->valueGenerationPlan = $valueGenerationPlan;
1751 364
    }
1752
1753
    /**
1754
     * Checks whether the class has a named query with the given query name.
1755
     *
1756
     * @param string $queryName
1757
     *
1758
     * @return boolean
1759
     */
1760 2
    public function hasNamedQuery($queryName) : bool
1761
    {
1762 2
        return isset($this->namedQueries[$queryName]);
1763
    }
1764
1765
    /**
1766
     * Checks whether the class has a named native query with the given query name.
1767
     *
1768
     * @param string $queryName
1769
     *
1770
     * @return boolean
1771
     */
1772 1
    public function hasNamedNativeQuery($queryName) : bool
1773
    {
1774 1
        return isset($this->namedNativeQueries[$queryName]);
1775
    }
1776
1777
    /**
1778
     * Checks whether the class has a named native query with the given query name.
1779
     *
1780
     * @param string $name
1781
     *
1782
     * @return boolean
1783
     */
1784 1
    public function hasSqlResultSetMapping($name) : bool
1785
    {
1786 1
        return isset($this->sqlResultSetMappings[$name]);
1787
    }
1788
1789
    /**
1790
     * Marks this class as read only, no change tracking is applied to it.
1791
     *
1792
     * @return void
1793
     */
1794 2
    public function asReadOnly() : void
1795
    {
1796 2
        $this->readOnly = true;
1797 2
    }
1798
1799
    /**
1800
     * Whether this class is read only or not.
1801
     *
1802
     * @return bool
1803
     */
1804 448
    public function isReadOnly() : bool
1805
    {
1806 448
        return $this->readOnly;
1807
    }
1808
1809
    /**
1810
     * @return bool
1811
     */
1812 1066
    public function isVersioned() : bool
1813
    {
1814 1066
        return $this->versionProperty !== null;
1815
    }
1816
1817
    /**
1818
     * Map Embedded Class
1819
     *
1820
     * @param array $mapping
1821
     *
1822
     * @throws MappingException
1823
     * @return void
1824
     */
1825 1
    public function mapEmbedded(array $mapping) : void
1826
    {
1827
        /*if (isset($this->declaredProperties[$mapping['fieldName']])) {
1828
            throw MappingException::duplicateProperty($this->className, $this->getProperty($mapping['fieldName']));
1829
        }
1830
1831
        $this->embeddedClasses[$mapping['fieldName']] = [
1832
            'class'          => $this->fullyQualifiedClassName($mapping['class']),
1833
            'columnPrefix'   => $mapping['columnPrefix'],
1834
            'declaredField'  => $mapping['declaredField'] ?? null,
1835
            'originalField'  => $mapping['originalField'] ?? null,
1836
            'declaringClass' => $this,
1837
        ];*/
1838 1
    }
1839
1840
    /**
1841
     * Inline the embeddable class
1842
     *
1843
     * @param string        $property
1844
     * @param ClassMetadata $embeddable
1845
     */
1846
    public function inlineEmbeddable($property, ClassMetadata $embeddable) : void
1847
    {
1848
        /*foreach ($embeddable->fieldMappings as $fieldName => $fieldMapping) {
1849
            $fieldMapping['fieldName']     = $property . "." . $fieldName;
1850
            $fieldMapping['originalClass'] = $fieldMapping['originalClass'] ?? $embeddable->getClassName();
1851
            $fieldMapping['originalField'] = $fieldMapping['originalField'] ?? $fieldName;
1852
            $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
1853
                ? $property . '.' . $fieldMapping['declaredField']
1854
                : $property;
1855
1856
            if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
1857
                $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
1858
            } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
1859
                $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName(
1860
                    $property,
1861
                    $fieldMapping['columnName'],
1862
                    $this->reflectionClass->getName(),
1863
                    $embeddable->reflectionClass->getName()
1864
                );
1865
            }
1866
1867
            $this->mapField($fieldMapping);
1868
        }*/
1869
    }
1870
}
1871