Failed Conditions
CANCELLED  
Pull Request — master (#7095)
by Benjamin
10:13
created

validateAndCompleteToManyAssociationMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 2
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping;
6
7
use Doctrine\ORM\Cache\CacheException;
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\Mapping\Factory\NamingStrategy;
10
use Doctrine\ORM\Reflection\ReflectionService;
11
use Doctrine\ORM\Sequencing\Planning\ValueGenerationPlan;
12
use Doctrine\ORM\Utility\PersisterHelper;
13
use function array_diff;
14
use function array_filter;
15
use function array_intersect;
16
use function array_map;
17
use function array_merge;
18
use function class_exists;
19
use function count;
20
use function explode;
21
use function get_class;
22
use function in_array;
23
use function interface_exists;
24
use function is_subclass_of;
25
use function method_exists;
26
use function spl_object_id;
27
use function sprintf;
28
use function strpos;
29
30
/**
31
 * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
32
 * of an entity and its associations.
33
 *
34
 */
35
class ClassMetadata extends ComponentMetadata implements TableOwner
36
{
37
    /**
38
     * The name of the custom repository class used for the entity class.
39
     * (Optional).
40
     *
41
     * @var string
42
     */
43
    protected $customRepositoryClassName;
44
45
    /**
46
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
47
     *
48
     * @var bool
49
     */
50
    public $isMappedSuperclass = false;
51
52
    /**
53
     * READ-ONLY: Whether this class describes the mapping of an embeddable class.
54
     *
55
     * @var bool
56
     */
57
    public $isEmbeddedClass = false;
58
59
    /**
60
     * Whether this class describes the mapping of a read-only class.
61
     * That means it is never considered for change-tracking in the UnitOfWork.
62
     * It is a very helpful performance optimization for entities that are immutable,
63
     * either in your domain or through the relation database (coming from a view,
64
     * or a history table for example).
65
     *
66
     * @var bool
67
     */
68
    private $readOnly = false;
69
70
    /**
71
     * The names of all subclasses (descendants).
72
     *
73
     * @var string[]
74
     */
75
    protected $subClasses = [];
76
77
    /**
78
     * READ-ONLY: The names of all embedded classes based on properties.
79
     *
80
     * @var string[]
81
     */
82
    //public $embeddedClasses = [];
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
83
84
    /**
85
     * READ-ONLY: The registered lifecycle callbacks for entities of this class.
86
     *
87
     * @var string[][]
88
     */
89
    public $lifecycleCallbacks = [];
90
91
    /**
92
     * READ-ONLY: The registered entity listeners.
93
     *
94
     * @var mixed[][]
95
     */
96
    public $entityListeners = [];
97
98
    /**
99
     * READ-ONLY: The field names of all fields that are part of the identifier/primary key
100
     * of the mapped entity class.
101
     *
102
     * @var string[]
103
     */
104
    public $identifier = [];
105
106
    /**
107
     * READ-ONLY: The inheritance mapping type used by the class.
108
     *
109
     * @var string
110
     */
111
    public $inheritanceType = InheritanceType::NONE;
112
113
    /**
114
     * READ-ONLY: The policy used for change-tracking on entities of this class.
115
     *
116
     * @var string
117
     */
118
    public $changeTrackingPolicy = ChangeTrackingPolicy::DEFERRED_IMPLICIT;
119
120
    /**
121
     * READ-ONLY: The discriminator value of this class.
122
     *
123
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
124
     * where a discriminator column is used.</b>
125
     *
126
     * @var mixed
127
     *
128
     * @see discriminatorColumn
129
     */
130
    public $discriminatorValue;
131
132
    /**
133
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
134
     *
135
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
136
     * where a discriminator column is used.</b>
137
     *
138
     * @var string[]
139
     *
140
     * @see discriminatorColumn
141
     */
142
    public $discriminatorMap = [];
143
144
    /**
145
     * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
146
     * inheritance mappings.
147
     *
148
     * @var DiscriminatorColumnMetadata
149
     */
150
    public $discriminatorColumn;
151
152
    /**
153
     * READ-ONLY: The primary table metadata.
154
     *
155
     * @var TableMetadata
156
     */
157
    public $table;
158
159
    /**
160
     * READ-ONLY: An array of field names. Used to look up field names from column names.
161
     * Keys are column names and values are field names.
162
     *
163
     * @var string[]
164
     */
165
    public $fieldNames = [];
166
167
    /**
168
     * READ-ONLY: The field which is used for versioning in optimistic locking (if any).
169
     *
170
     * @var FieldMetadata|null
171
     */
172
    public $versionProperty;
173
174
    /**
175
     * NamingStrategy determining the default column and table names.
176
     *
177
     * @var NamingStrategy
178
     */
179
    protected $namingStrategy;
180
181
    /**
182
     * Value generation plan is responsible for generating values for auto-generated fields.
183
     *
184
     * @var ValueGenerationPlan
185
     */
186
    protected $valueGenerationPlan;
187
188
    /**
189
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
190
     * metadata of the class with the given name.
191
     *
192
     * @param string $entityName The name of the entity class.
193
     */
194 456
    public function __construct(
195
        string $entityName,
196
        ClassMetadataBuildingContext $metadataBuildingContext
197
    ) {
198 456
        parent::__construct($entityName, $metadataBuildingContext);
199
200 456
        $this->namingStrategy = $metadataBuildingContext->getNamingStrategy();
201 456
    }
202
203 2
    public function setClassName(string $className)
204
    {
205 2
        $this->className = $className;
206 2
    }
207
208
    public function getColumnsIterator() : \ArrayIterator
209
    {
210
        $iterator = parent::getColumnsIterator();
211
212
        if ($this->discriminatorColumn) {
213
            $iterator->offsetSet($this->discriminatorColumn->getColumnName(), $this->discriminatorColumn);
0 ignored issues
show
Bug introduced by
$this->discriminatorColumn of type Doctrine\ORM\Mapping\DiscriminatorColumnMetadata is incompatible with the type string expected by parameter $newval of ArrayIterator::offsetSet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

213
            $iterator->offsetSet($this->discriminatorColumn->getColumnName(), /** @scrutinizer ignore-type */ $this->discriminatorColumn);
Loading history...
214
        }
215
216
        return $iterator;
217
    }
218
219 11
    public function getAncestorsIterator() : \ArrayIterator
220
    {
221 11
        $ancestors = new \ArrayIterator();
222 11
        $parent    = $this;
223
224 11
        while (($parent = $parent->parent) !== null) {
225 8
            if ($parent instanceof ClassMetadata && $parent->isMappedSuperclass) {
226 1
                continue;
227
            }
228
229 7
            $ancestors->append($parent);
230
        }
231
232 11
        return $ancestors;
233
    }
234
235 1252
    public function getRootClassName() : string
236
    {
237 1252
        return ($this->parent instanceof ClassMetadata && ! $this->parent->isMappedSuperclass)
238 403
            ? $this->parent->getRootClassName()
239 1252
            : $this->className
240
        ;
241
    }
242
243
    /**
244
     * Handles metadata cloning nicely.
245
     */
246 13
    public function __clone()
247
    {
248 13
        if ($this->cache) {
249 12
            $this->cache = clone $this->cache;
250
        }
251
252 13
        foreach ($this->declaredProperties as $name => $property) {
253 13
            $this->declaredProperties[$name] = clone $property;
254
        }
255 13
    }
256
257
    /**
258
     * Creates a string representation of this instance.
259
     *
260
     * @return string The string representation of this instance.
261
     *
262
     * @todo Construct meaningful string representation.
263
     */
264
    public function __toString()
265
    {
266
        return __CLASS__ . '@' . spl_object_id($this);
267
    }
268
269
    /**
270
     * Determines which fields get serialized.
271
     *
272
     * It is only serialized what is necessary for best unserialization performance.
273
     * That means any metadata properties that are not set or empty or simply have
274
     * their default value are NOT serialized.
275
     *
276
     * Parts that are also NOT serialized because they can not be properly unserialized:
277
     * - reflectionClass
278
     *
279
     * @return string[] The names of all the fields that should be serialized.
280
     */
281 5
    public function __sleep()
282
    {
283 5
        $serialized = [];
284
285
        // This metadata is always serialized/cached.
286 5
        $serialized = array_merge($serialized, [
287 5
            'declaredProperties',
288
            'fieldNames',
289
            //'embeddedClasses',
290
            'identifier',
291
            'className',
292
            'parent',
293
            'table',
294
            'valueGenerationPlan',
295
        ]);
296
297
        // The rest of the metadata is only serialized if necessary.
298 5
        if ($this->changeTrackingPolicy !== ChangeTrackingPolicy::DEFERRED_IMPLICIT) {
299
            $serialized[] = 'changeTrackingPolicy';
300
        }
301
302 5
        if ($this->customRepositoryClassName) {
303 1
            $serialized[] = 'customRepositoryClassName';
304
        }
305
306 5
        if ($this->inheritanceType !== InheritanceType::NONE) {
307 1
            $serialized[] = 'inheritanceType';
308 1
            $serialized[] = 'discriminatorColumn';
309 1
            $serialized[] = 'discriminatorValue';
310 1
            $serialized[] = 'discriminatorMap';
311 1
            $serialized[] = 'subClasses';
312
        }
313
314 5
        if ($this->isMappedSuperclass) {
315
            $serialized[] = 'isMappedSuperclass';
316
        }
317
318 5
        if ($this->isEmbeddedClass) {
319
            $serialized[] = 'isEmbeddedClass';
320
        }
321
322 5
        if ($this->isVersioned()) {
323
            $serialized[] = 'versionProperty';
324
        }
325
326 5
        if ($this->lifecycleCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lifecycleCallbacks of type array<mixed,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...
327
            $serialized[] = 'lifecycleCallbacks';
328
        }
329
330 5
        if ($this->entityListeners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityListeners of type array<mixed,array<mixed,mixed>> 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...
331 1
            $serialized[] = 'entityListeners';
332
        }
333
334 5
        if ($this->cache) {
335
            $serialized[] = 'cache';
336
        }
337
338 5
        if ($this->readOnly) {
339 1
            $serialized[] = 'readOnly';
340
        }
341
342 5
        return $serialized;
343
    }
344
345
    /**
346
     * Restores some state that can not be serialized/unserialized.
347
     */
348 1622
    public function wakeupReflection(ReflectionService $reflectionService) : void
349
    {
350
        // Restore ReflectionClass and properties
351 1622
        $this->reflectionClass = $reflectionService->getClass($this->className);
352
353 1622
        if (! $this->reflectionClass) {
354
            return;
355
        }
356
357 1622
        $this->className = $this->reflectionClass->getName();
358
359 1622
        foreach ($this->declaredProperties as $property) {
360
            /** @var Property $property */
361 1621
            $property->wakeupReflection($reflectionService);
362
        }
363 1622
    }
364
365
    /**
366
     * Validates Identifier.
367
     *
368
     * @throws MappingException
369
     */
370 360
    public function validateIdentifier() : void
371
    {
372 360
        if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
373 28
            return;
374
        }
375
376
        // Verify & complete identifier mapping
377 360
        if (! $this->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identifier of type 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...
378 4
            throw MappingException::identifierRequired($this->className);
379
        }
380
381 356
        $explicitlyGeneratedProperties = array_filter($this->declaredProperties, function (Property $property) : bool {
382 356
            return $property instanceof FieldMetadata
383 356
                && $property->isPrimaryKey()
384 356
                && $property->hasValueGenerator();
385 356
        });
386
387 356
        if ($explicitlyGeneratedProperties && $this->isIdentifierComposite()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $explicitlyGeneratedProperties 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
            throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->className);
389
        }
390 356
    }
391
392
    /**
393
     * Validates association targets actually exist.
394
     *
395
     * @throws MappingException
396
     */
397 359
    public function validateAssociations() : void
398
    {
399 359
        array_map(
400 359
            function (Property $property) {
401 359
                if (! ($property instanceof AssociationMetadata)) {
402 356
                    return;
403
                }
404
405 246
                $targetEntity = $property->getTargetEntity();
406
407 246
                if (! class_exists($targetEntity)) {
408 1
                    throw MappingException::invalidTargetEntityClass($targetEntity, $this->className, $property->getName());
409
                }
410 359
            },
411 359
            $this->declaredProperties
412
        );
413 358
    }
414
415
    /**
416
     * Validates lifecycle callbacks.
417
     *
418
     * @throws MappingException
419
     */
420 359
    public function validateLifecycleCallbacks(ReflectionService $reflectionService) : void
421
    {
422 359
        foreach ($this->lifecycleCallbacks as $callbacks) {
423
            /** @var array $callbacks */
424 11
            foreach ($callbacks as $callbackFuncName) {
425 11
                if (! $reflectionService->hasPublicMethod($this->className, $callbackFuncName)) {
426 11
                    throw MappingException::lifecycleCallbackMethodNotFound($this->className, $callbackFuncName);
427
                }
428
            }
429
        }
430 358
    }
431
432
    /**
433
     * Sets the change tracking policy used by this class.
434
     */
435 103
    public function setChangeTrackingPolicy(string $policy) : void
436
    {
437 103
        $this->changeTrackingPolicy = $policy;
438 103
    }
439
440
    /**
441
     * Checks whether a field is part of the identifier/primary key field(s).
442
     *
443
     * @param string $fieldName The field name.
444
     *
445
     * @return bool TRUE if the field is part of the table identifier/primary key field(s), FALSE otherwise.
446
     */
447 1015
    public function isIdentifier(string $fieldName) : bool
448
    {
449 1015
        if (! $this->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identifier of type 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...
450 1
            return false;
451
        }
452
453 1014
        if (! $this->isIdentifierComposite()) {
454 1010
            return $fieldName === $this->identifier[0];
455
        }
456
457 90
        return in_array($fieldName, $this->identifier, true);
458
    }
459
460 1199
    public function isIdentifierComposite() : bool
461
    {
462 1199
        return isset($this->identifier[1]);
463
    }
464
465
    /**
466
     * Gets the result set mapping.
467
     *
468
     * @see ClassMetadata::$sqlResultSetMappings
469
     *
470
     * @param string $name The result set mapping name.
471
     *
472
     * @return mixed[]
473
     *
474
     * @throws MappingException
475
     */
476
    public function getSqlResultSetMapping($name)
477
    {
478
        if (! isset($this->sqlResultSetMappings[$name])) {
479
            throw MappingException::resultMappingNotFound($this->className, $name);
480
        }
481
482
        return $this->sqlResultSetMappings[$name];
483
    }
484
485
    /**
486
     * Gets all sql result set mappings of the class.
487
     *
488
     * @return mixed[][]
489
     */
490
    public function getSqlResultSetMappings()
491
    {
492
        return $this->sqlResultSetMappings;
493
    }
494
495
    /**
496
     * Validates & completes the basic mapping information for field mapping.
497
     *
498
     * @throws MappingException If something is wrong with the mapping.
499
     */
500 404
    protected function validateAndCompleteFieldMapping(FieldMetadata $property)
501
    {
502 404
        $fieldName  = $property->getName();
503 404
        $columnName = $property->getColumnName();
504
505 404
        if (empty($columnName)) {
506 341
            $columnName = $this->namingStrategy->propertyToColumnName($fieldName, $this->className);
507
508 341
            $property->setColumnName($columnName);
509
        }
510
511 404
        if (! $this->isMappedSuperclass) {
512 397
            $property->setTableName($this->getTableName());
513
        }
514
515
        // Check for already declared column
516 404
        if (isset($this->fieldNames[$columnName]) ||
517 404
            ($this->discriminatorColumn !== null && $this->discriminatorColumn->getColumnName() === $columnName)) {
518 2
            throw MappingException::duplicateColumnName($this->className, $columnName);
519
        }
520
521
        // Complete id mapping
522 403
        if ($property->isPrimaryKey()) {
523 389
            if ($this->versionProperty !== null && $this->versionProperty->getName() === $fieldName) {
524
                throw MappingException::cannotVersionIdField($this->className, $fieldName);
525
            }
526
527 389
            if ($property->getType()->canRequireSQLConversion()) {
528
                throw MappingException::sqlConversionNotAllowedForPrimaryKeyProperties($this->className, $property);
529
            }
530
531 389
            if (! in_array($fieldName, $this->identifier, true)) {
532 389
                $this->identifier[] = $fieldName;
533
            }
534
        }
535
536 403
        $this->fieldNames[$columnName] = $fieldName;
537 403
    }
538
539
    /**
540
     * Validates & completes the basic mapping information for field mapping.
541
     *
542
     * @throws MappingException If something is wrong with the mapping.
543
     */
544 20
    protected function validateAndCompleteVersionFieldMapping(VersionFieldMetadata $property)
545
    {
546 20
        $this->versionProperty = $property;
547
548 20
        $options = $property->getOptions();
549
550 20
        if (isset($options['default'])) {
551
            return;
552
        }
553
554 20
        if (in_array($property->getTypeName(), ['integer', 'bigint', 'smallint'], true)) {
555 18
            $property->setOptions(array_merge($options, ['default' => 1]));
556
557 18
            return;
558
        }
559
560 3
        if ($property->getTypeName() === 'datetime') {
561 2
            $property->setOptions(array_merge($options, ['default' => 'CURRENT_TIMESTAMP']));
562
563 2
            return;
564
        }
565
566 1
        throw MappingException::unsupportedOptimisticLockingType($property->getType());
567
    }
568
569
    /**
570
     * Validates & completes the basic mapping information that is common to all
571
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
572
     *
573
     * @throws MappingException If something is wrong with the mapping.
574
     * @throws CacheException   If entity is not cacheable.
575
     */
576 284
    protected function validateAndCompleteAssociationMapping(AssociationMetadata $property)
577
    {
578 284
        $fieldName    = $property->getName();
579 284
        $targetEntity = $property->getTargetEntity();
580
581 284
        if (! $targetEntity) {
582
            throw MappingException::missingTargetEntity($fieldName);
583
        }
584
585 284
        $property->setSourceEntity($this->className);
586 284
        $property->setTargetEntity($targetEntity);
587
588
        // Complete id mapping
589 284
        if ($property->isPrimaryKey()) {
590 44
            if ($property->isOrphanRemoval()) {
591 1
                throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->className, $fieldName);
592
            }
593
594 43
            if (! in_array($property->getName(), $this->identifier, true)) {
595 43
                if ($property instanceof ToOneAssociationMetadata && count($property->getJoinColumns()) >= 2) {
596
                    throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
597
                        $property->getTargetEntity(),
598
                        $this->className,
599
                        $fieldName
600
                    );
601
                }
602
603 43
                $this->identifier[] = $property->getName();
604
            }
605
606 43
            if ($this->cache && ! $property->getCache()) {
607 2
                throw CacheException::nonCacheableEntityAssociation($this->className, $fieldName);
608
            }
609
610 41
            if ($property instanceof ToManyAssociationMetadata) {
611 1
                throw MappingException::illegalToManyIdentifierAssociation($this->className, $property->getName());
612
            }
613
        }
614
615
        // Cascades
616 280
        $cascadeTypes = ['remove', 'persist', 'refresh'];
617 280
        $cascades     = array_map('strtolower', $property->getCascade());
618
619 280
        if (in_array('all', $cascades, true)) {
620 6
            $cascades = $cascadeTypes;
621
        }
622
623 280
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
624 1
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
625
626 1
            throw MappingException::invalidCascadeOption($diffCascades, $this->className, $fieldName);
627
        }
628
629 279
        $property->setCascade($cascades);
630 279
    }
631
632
    /**
633
     * Validates & completes a to-one association mapping.
634
     *
635
     * @param ToOneAssociationMetadata $property The association mapping to validate & complete.
636
     *
637
     * @throws \RuntimeException
638
     * @throws MappingException
639
     */
640 244
    protected function validateAndCompleteToOneAssociationMetadata(ToOneAssociationMetadata $property)
641
    {
642 244
        $fieldName = $property->getName();
643
644 244
        if ($property->isOwningSide()) {
645 240
            if (empty($property->getJoinColumns())) {
646
                // Apply default join column
647 85
                $property->addJoinColumn(new JoinColumnMetadata());
648
            }
649
650 240
            $uniqueConstraintColumns = [];
651
652 240
            foreach ($property->getJoinColumns() as $joinColumn) {
653
                /** @var JoinColumnMetadata $joinColumn */
654 240
                if ($property instanceof OneToOneAssociationMetadata && $this->inheritanceType !== InheritanceType::SINGLE_TABLE) {
655 109
                    if (count($property->getJoinColumns()) === 1) {
656 107
                        if (! $property->isPrimaryKey()) {
657 107
                            $joinColumn->setUnique(true);
658
                        }
659
                    } else {
660 2
                        $uniqueConstraintColumns[] = $joinColumn->getColumnName();
661
                    }
662
                }
663
664 240
                $joinColumn->setTableName(! $this->isMappedSuperclass ? $this->getTableName() : null);
665
666 240
                if (! $joinColumn->getColumnName()) {
667 98
                    $joinColumn->setColumnName($this->namingStrategy->joinColumnName($fieldName, $this->className));
668
                }
669
670 240
                if (! $joinColumn->getReferencedColumnName()) {
671 85
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
672
                }
673
674 240
                $this->fieldNames[$joinColumn->getColumnName()] = $fieldName;
675
            }
676
677 240
            if ($uniqueConstraintColumns) {
678 2
                if (! $this->table) {
679
                    throw new \RuntimeException(
680
                        'ClassMetadata::setTable() has to be called before defining a one to one relationship.'
681
                    );
682
                }
683
684 2
                $this->table->addUniqueConstraint(
685
                    [
686 2
                        'name'    => sprintf('%s_uniq', $fieldName),
687 2
                        'columns' => $uniqueConstraintColumns,
688
                        'options' => [],
689
                        'flags'   => [],
690
                    ]
691
                );
692
            }
693
        }
694
695 244
        if ($property->isOrphanRemoval()) {
696 7
            $cascades = $property->getCascade();
697
698 7
            if (! in_array('remove', $cascades, true)) {
699 6
                $cascades[] = 'remove';
700
701 6
                $property->setCascade($cascades);
702
            }
703
704
            // @todo guilhermeblanco where is this used?
705
            // @todo guilhermeblanco Shouldn￿'t we iterate through JoinColumns to set non-uniqueness?
706
            //$property->setUnique(false);
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
707
        }
708
709 244
        if ($property->isPrimaryKey() && ! $property->isOwningSide()) {
710 1
            throw MappingException::illegalInverseIdentifierAssociation($this->className, $fieldName);
711
        }
712 243
    }
713
714
    /**
715
     * Validates & completes a to-many association mapping.
716
     *
717
     * @param ToManyAssociationMetadata $property The association mapping to validate & complete.
718
     *
719
     * @throws MappingException
720
     */
721 170
    protected function validateAndCompleteToManyAssociationMetadata(ToManyAssociationMetadata $property)
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

721
    protected function validateAndCompleteToManyAssociationMetadata(/** @scrutinizer ignore-unused */ ToManyAssociationMetadata $property)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
722
    {
723
        // Do nothing
724 170
    }
725
726
    /**
727
     * Validates & completes a one-to-one association mapping.
728
     *
729
     * @param OneToOneAssociationMetadata $property The association mapping to validate & complete.
730
     */
731 126
    protected function validateAndCompleteOneToOneMapping(OneToOneAssociationMetadata $property)
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

731
    protected function validateAndCompleteOneToOneMapping(/** @scrutinizer ignore-unused */ OneToOneAssociationMetadata $property)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
732
    {
733
        // Do nothing
734 126
    }
735
736
    /**
737
     * Validates & completes a many-to-one association mapping.
738
     *
739
     * @param ManyToOneAssociationMetadata $property The association mapping to validate & complete.
740
     *
741
     * @throws MappingException
742
     */
743 145
    protected function validateAndCompleteManyToOneMapping(ManyToOneAssociationMetadata $property)
744
    {
745
        // A many-to-one mapping is essentially a one-one backreference
746 145
        if ($property->isOrphanRemoval()) {
747
            throw MappingException::illegalOrphanRemoval($this->className, $property->getName());
748
        }
749 145
    }
750
751
    /**
752
     * Validates & completes a one-to-many association mapping.
753
     *
754
     * @param OneToManyAssociationMetadata $property The association mapping to validate & complete.
755
     *
756
     * @throws MappingException
757
     */
758 114
    protected function validateAndCompleteOneToManyMapping(OneToManyAssociationMetadata $property)
759
    {
760
        // OneToMany MUST have mappedBy
761 114
        if (! $property->getMappedBy()) {
762
            throw MappingException::oneToManyRequiresMappedBy($property->getName());
763
        }
764
765 114
        if ($property->isOrphanRemoval()) {
766 19
            $cascades = $property->getCascade();
767
768 19
            if (! in_array('remove', $cascades, true)) {
769 16
                $cascades[] = 'remove';
770
771 16
                $property->setCascade($cascades);
772
            }
773
        }
774 114
    }
775
776
    /**
777
     * Validates & completes a many-to-many association mapping.
778
     *
779
     * @param ManyToManyAssociationMetadata $property The association mapping to validate & complete.
780
     *
781
     * @throws MappingException
782
     */
783 109
    protected function validateAndCompleteManyToManyMapping(ManyToManyAssociationMetadata $property)
784
    {
785 109
        if ($property->isOwningSide()) {
786
            // owning side MUST have a join table
787 97
            $joinTable = $property->getJoinTable() ?: new JoinTableMetadata();
788
789 97
            $property->setJoinTable($joinTable);
790
791 97
            if (! $joinTable->getName()) {
792 18
                $joinTableName = $this->namingStrategy->joinTableName(
793 18
                    $property->getSourceEntity(),
794 18
                    $property->getTargetEntity(),
795 18
                    $property->getName()
796
                );
797
798 18
                $joinTable->setName($joinTableName);
799
            }
800
801 97
            $selfReferencingEntityWithoutJoinColumns = $property->getSourceEntity() === $property->getTargetEntity() && ! $joinTable->hasColumns();
802
803 97
            if (! $joinTable->getJoinColumns()) {
804 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
805 16
                $sourceReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'source' : $referencedColumnName;
806 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getSourceEntity(), $sourceReferenceName);
807 16
                $joinColumn           = new JoinColumnMetadata();
808
809 16
                $joinColumn->setColumnName($columnName);
810 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
811 16
                $joinColumn->setOnDelete('CASCADE');
812
813 16
                $joinTable->addJoinColumn($joinColumn);
814
            }
815
816 97
            if (! $joinTable->getInverseJoinColumns()) {
817 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
818 16
                $targetReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'target' : $referencedColumnName;
819 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getTargetEntity(), $targetReferenceName);
820 16
                $joinColumn           = new JoinColumnMetadata();
821
822 16
                $joinColumn->setColumnName($columnName);
823 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
824 16
                $joinColumn->setOnDelete('CASCADE');
825
826 16
                $joinTable->addInverseJoinColumn($joinColumn);
827
            }
828
829 97
            foreach ($joinTable->getJoinColumns() as $joinColumn) {
830
                /** @var JoinColumnMetadata $joinColumn */
831 97
                if (! $joinColumn->getReferencedColumnName()) {
832 2
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
833
                }
834
835 97
                $referencedColumnName = $joinColumn->getReferencedColumnName();
836
837 97
                if (! $joinColumn->getColumnName()) {
838 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
839 2
                        $property->getSourceEntity(),
840 2
                        $referencedColumnName
841
                    );
842
843 97
                    $joinColumn->setColumnName($columnName);
844
                }
845
            }
846
847 97
            foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) {
848
                /** @var JoinColumnMetadata $inverseJoinColumn */
849 97
                if (! $inverseJoinColumn->getReferencedColumnName()) {
850 2
                    $inverseJoinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
851
                }
852
853 97
                $referencedColumnName = $inverseJoinColumn->getReferencedColumnName();
854
855 97
                if (! $inverseJoinColumn->getColumnName()) {
856 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
857 2
                        $property->getTargetEntity(),
858 2
                        $referencedColumnName
859
                    );
860
861 97
                    $inverseJoinColumn->setColumnName($columnName);
862
                }
863
            }
864
        }
865 109
    }
866
867
    /**
868
     * {@inheritDoc}
869
     */
870 394
    public function getIdentifierFieldNames()
871
    {
872 394
        return $this->identifier;
873
    }
874
875
    /**
876
     * Gets the name of the single id field. Note that this only works on
877
     * entity classes that have a single-field pk.
878
     *
879
     * @return string
880
     *
881
     * @throws MappingException If the class has a composite primary key.
882
     */
883 149
    public function getSingleIdentifierFieldName()
884
    {
885 149
        if ($this->isIdentifierComposite()) {
886 1
            throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->className);
887
        }
888
889 148
        if (! isset($this->identifier[0])) {
890 1
            throw MappingException::noIdDefined($this->className);
891
        }
892
893 147
        return $this->identifier[0];
894
    }
895
896
    /**
897
     * INTERNAL:
898
     * Sets the mapped identifier/primary key fields of this class.
899
     * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
900
     *
901
     * @param mixed[] $identifier
902
     */
903 100
    public function setIdentifier(array $identifier)
904
    {
905 100
        $this->identifier = $identifier;
906 100
    }
907
908
    /**
909
     * {@inheritDoc}
910
     */
911 1040
    public function getIdentifier()
912
    {
913 1040
        return $this->identifier;
914
    }
915
916
    /**
917
     * {@inheritDoc}
918
     */
919 184
    public function hasField($fieldName)
920
    {
921 184
        return isset($this->declaredProperties[$fieldName])
922 184
            && $this->declaredProperties[$fieldName] instanceof FieldMetadata;
923
    }
924
925
    /**
926
     * Returns an array with identifier column names and their corresponding ColumnMetadata.
927
     *
928
     * @return ColumnMetadata[]
929
     */
930 439
    public function getIdentifierColumns(EntityManagerInterface $em) : array
931
    {
932 439
        $columns = [];
933
934 439
        foreach ($this->identifier as $idProperty) {
935 439
            $property = $this->getProperty($idProperty);
936
937 439
            if ($property instanceof FieldMetadata) {
938 434
                $columns[$property->getColumnName()] = $property;
939
940 434
                continue;
941
            }
942
943
            /** @var AssociationMetadata $property */
944
945
            // Association defined as Id field
946 24
            $targetClass = $em->getClassMetadata($property->getTargetEntity());
947
948 24
            if (! $property->isOwningSide()) {
949
                $property    = $targetClass->getProperty($property->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

949
                /** @scrutinizer ignore-call */ 
950
                $property    = $targetClass->getProperty($property->getMappedBy());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
950
                $targetClass = $em->getClassMetadata($property->getTargetEntity());
951
            }
952
953 24
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
954
                ? $property->getJoinTable()->getInverseJoinColumns()
955 24
                : $property->getJoinColumns()
956
            ;
957
958 24
            foreach ($joinColumns as $joinColumn) {
959
                /** @var JoinColumnMetadata $joinColumn */
960 24
                $columnName           = $joinColumn->getColumnName();
961 24
                $referencedColumnName = $joinColumn->getReferencedColumnName();
962
963 24
                if (! $joinColumn->getType()) {
964 12
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

964
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $em));
Loading history...
965
                }
966
967 24
                $columns[$columnName] = $joinColumn;
968
            }
969
        }
970
971 439
        return $columns;
972
    }
973
974
    /**
975
     * Gets the name of the primary table.
976
     */
977 1578
    public function getTableName() : ?string
978
    {
979 1578
        return $this->table->getName();
980
    }
981
982
    /**
983
     * Gets primary table's schema name.
984
     */
985 14
    public function getSchemaName() : ?string
986
    {
987 14
        return $this->table->getSchema();
988
    }
989
990
    /**
991
     * Gets the table name to use for temporary identifier tables of this class.
992
     */
993 7
    public function getTemporaryIdTableName() : string
994
    {
995 7
        $schema = $this->getSchemaName() === null
996 6
            ? ''
997 7
            : $this->getSchemaName() . '_'
998
        ;
999
1000
        // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
1001 7
        return $schema . $this->getTableName() . '_id_tmp';
1002
    }
1003
1004
    /**
1005
     * Sets the mapped subclasses of this class.
1006
     *
1007
     * @todo guilhermeblanco Only used for ClassMetadataTest. Remove if possible!
1008
     *
1009
     * @param string[] $subclasses The names of all mapped subclasses.
1010
     */
1011 4
    public function setSubclasses(array $subclasses) : void
1012
    {
1013 4
        foreach ($subclasses as $subclass) {
1014 3
            $this->subClasses[] = $subclass;
1015
        }
1016 4
    }
1017
1018
    /**
1019
     * @return string[]
1020
     */
1021 1071
    public function getSubClasses() : array
1022
    {
1023 1071
        return $this->subClasses;
1024
    }
1025
1026
    /**
1027
     * Sets the inheritance type used by the class and its subclasses.
1028
     *
1029
     * @param int $type
1030
     *
1031
     * @throws MappingException
1032
     */
1033 118
    public function setInheritanceType($type) : void
1034
    {
1035 118
        if (! $this->isInheritanceType($type)) {
1036
            throw MappingException::invalidInheritanceType($this->className, $type);
1037
        }
1038
1039 118
        $this->inheritanceType = $type;
1040 118
    }
1041
1042
    /**
1043
     * Sets the override property mapping for an entity relationship.
1044
     *
1045
     * @throws \RuntimeException
1046
     * @throws MappingException
1047
     * @throws CacheException
1048
     */
1049 12
    public function setPropertyOverride(Property $property) : void
1050
    {
1051 12
        $fieldName = $property->getName();
1052
1053 12
        if (! isset($this->declaredProperties[$fieldName])) {
1054 2
            throw MappingException::invalidOverrideFieldName($this->className, $fieldName);
1055
        }
1056
1057 10
        $originalProperty          = $this->getProperty($fieldName);
1058 10
        $originalPropertyClassName = get_class($originalProperty);
1059
1060
        // If moving from transient to persistent, assume it's a new property
1061 10
        if ($originalPropertyClassName === TransientMetadata::class) {
1062 1
            unset($this->declaredProperties[$fieldName]);
1063
1064 1
            $this->addProperty($property);
1065
1066 1
            return;
1067
        }
1068
1069
        // Do not allow to change property type
1070 9
        if ($originalPropertyClassName !== get_class($property)) {
1071
            throw MappingException::invalidOverridePropertyType($this->className, $fieldName);
1072
        }
1073
1074
        // Do not allow to change version property
1075 9
        if ($originalProperty instanceof VersionFieldMetadata) {
1076
            throw MappingException::invalidOverrideVersionField($this->className, $fieldName);
1077
        }
1078
1079 9
        unset($this->declaredProperties[$fieldName]);
1080
1081 9
        if ($property instanceof FieldMetadata) {
1082
            // Unset defined fieldName prior to override
1083 5
            unset($this->fieldNames[$originalProperty->getColumnName()]);
0 ignored issues
show
Bug introduced by
The method getColumnName() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1083
            unset($this->fieldNames[$originalProperty->/** @scrutinizer ignore-call */ getColumnName()]);
Loading history...
1084
1085
            // Revert what should not be allowed to change
1086 5
            $property->setDeclaringClass($originalProperty->getDeclaringClass());
1087 5
            $property->setPrimaryKey($originalProperty->isPrimaryKey());
1088 9
        } elseif ($property instanceof AssociationMetadata) {
1089
            // Unset all defined fieldNames prior to override
1090 9
            if ($originalProperty instanceof ToOneAssociationMetadata && $originalProperty->isOwningSide()) {
1091 5
                foreach ($originalProperty->getJoinColumns() as $joinColumn) {
1092 5
                    unset($this->fieldNames[$joinColumn->getColumnName()]);
1093
                }
1094
            }
1095
1096
            // Override what it should be allowed to change
1097 9
            if ($property->getInversedBy()) {
1098 2
                $originalProperty->setInversedBy($property->getInversedBy());
0 ignored issues
show
Bug introduced by
The method setInversedBy() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1098
                $originalProperty->/** @scrutinizer ignore-call */ 
1099
                                   setInversedBy($property->getInversedBy());
Loading history...
1099
            }
1100
1101 9
            if ($property->getFetchMode() !== $originalProperty->getFetchMode()) {
0 ignored issues
show
Bug introduced by
The method getFetchMode() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1101
            if ($property->getFetchMode() !== $originalProperty->/** @scrutinizer ignore-call */ getFetchMode()) {
Loading history...
1102 2
                $originalProperty->setFetchMode($property->getFetchMode());
0 ignored issues
show
Bug introduced by
The method setFetchMode() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1102
                $originalProperty->/** @scrutinizer ignore-call */ 
1103
                                   setFetchMode($property->getFetchMode());
Loading history...
1103
            }
1104
1105 9
            if ($originalProperty instanceof ToOneAssociationMetadata && $property->getJoinColumns()) {
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1105
            if ($originalProperty instanceof ToOneAssociationMetadata && $property->/** @scrutinizer ignore-call */ getJoinColumns()) {
Loading history...
1106 5
                $originalProperty->setJoinColumns($property->getJoinColumns());
1107 8
            } elseif ($originalProperty instanceof ManyToManyAssociationMetadata && $property->getJoinTable()) {
0 ignored issues
show
Bug introduced by
The method getJoinTable() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1107
            } elseif ($originalProperty instanceof ManyToManyAssociationMetadata && $property->/** @scrutinizer ignore-call */ getJoinTable()) {
Loading history...
1108 4
                $originalProperty->setJoinTable($property->getJoinTable());
1109
            }
1110
1111 9
            $property = $originalProperty;
1112
        }
1113
1114 9
        $this->addProperty($property);
0 ignored issues
show
Bug introduced by
It seems like $property can also be of type null; however, parameter $property of Doctrine\ORM\Mapping\ClassMetadata::addProperty() does only seem to accept Doctrine\ORM\Mapping\Property, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1114
        $this->addProperty(/** @scrutinizer ignore-type */ $property);
Loading history...
1115 9
    }
1116
1117
    /**
1118
     * Checks if this entity is the root in any entity-inheritance-hierarchy.
1119
     *
1120
     * @return bool
1121
     */
1122 337
    public function isRootEntity()
1123
    {
1124 337
        return $this->className === $this->getRootClassName();
1125
    }
1126
1127
    /**
1128
     * Checks whether a mapped field is inherited from a superclass.
1129
     *
1130
     * @param string $fieldName
1131
     *
1132
     * @return bool TRUE if the field is inherited, FALSE otherwise.
1133
     */
1134 618
    public function isInheritedProperty($fieldName)
1135
    {
1136 618
        $declaringClass = $this->declaredProperties[$fieldName]->getDeclaringClass();
1137
1138 618
        return ! ($declaringClass->className === $this->className);
1139
    }
1140
1141
    /**
1142
     * {@inheritdoc}
1143
     */
1144 438
    public function setTable(TableMetadata $table) : void
1145
    {
1146 438
        $this->table = $table;
1147
1148 438
        if (empty($table->getName())) {
1149
            $table->setName($this->namingStrategy->classToTableName($this->className));
1150
        }
1151 438
    }
1152
1153
    /**
1154
     * Checks whether the given type identifies an inheritance type.
1155
     *
1156
     * @param int $type
1157
     *
1158
     * @return bool TRUE if the given type identifies an inheritance type, FALSe otherwise.
1159
     */
1160 118
    private function isInheritanceType($type)
1161
    {
1162 118
        return $type === InheritanceType::NONE
1163 92
            || $type === InheritanceType::SINGLE_TABLE
1164 50
            || $type === InheritanceType::JOINED
1165 118
            || $type === InheritanceType::TABLE_PER_CLASS;
1166
    }
1167
1168 905
    public function getColumn(string $columnName) : ?LocalColumnMetadata
1169
    {
1170 905
        foreach ($this->declaredProperties as $property) {
1171 905
            if ($property instanceof LocalColumnMetadata && $property->getColumnName() === $columnName) {
1172 905
                return $property;
1173
            }
1174
        }
1175
1176
        return null;
1177
    }
1178
1179
    /**
1180
     * Add a property mapping.
1181
     *
1182
     * @throws \RuntimeException
1183
     * @throws MappingException
1184
     * @throws CacheException
1185
     */
1186 428
    public function addProperty(Property $property)
1187
    {
1188 428
        $fieldName = $property->getName();
1189
1190
        // Check for empty field name
1191 428
        if (empty($fieldName)) {
1192 1
            throw MappingException::missingFieldName($this->className);
1193
        }
1194
1195 427
        $property->setDeclaringClass($this);
1196
1197
        switch (true) {
1198 427
            case ($property instanceof VersionFieldMetadata):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1199 20
                $this->validateAndCompleteFieldMapping($property);
1200 20
                $this->validateAndCompleteVersionFieldMapping($property);
1201 19
                break;
1202
1203 426
            case ($property instanceof FieldMetadata):
1204 403
                $this->validateAndCompleteFieldMapping($property);
1205 402
                break;
1206
1207 287
            case ($property instanceof OneToOneAssociationMetadata):
1208 128
                $this->validateAndCompleteAssociationMapping($property);
1209 127
                $this->validateAndCompleteToOneAssociationMetadata($property);
1210 126
                $this->validateAndCompleteOneToOneMapping($property);
1211 126
                break;
1212
1213 224
            case ($property instanceof OneToManyAssociationMetadata):
1214 114
                $this->validateAndCompleteAssociationMapping($property);
1215 114
                $this->validateAndCompleteToManyAssociationMetadata($property);
1216 114
                $this->validateAndCompleteOneToManyMapping($property);
1217 114
                break;
1218
1219 220
            case ($property instanceof ManyToOneAssociationMetadata):
1220 148
                $this->validateAndCompleteAssociationMapping($property);
1221 145
                $this->validateAndCompleteToOneAssociationMetadata($property);
1222 145
                $this->validateAndCompleteManyToOneMapping($property);
1223 145
                break;
1224
1225 126
            case ($property instanceof ManyToManyAssociationMetadata):
1226 110
                $this->validateAndCompleteAssociationMapping($property);
1227 109
                $this->validateAndCompleteToManyAssociationMetadata($property);
1228 109
                $this->validateAndCompleteManyToManyMapping($property);
1229 109
                break;
1230
1231
            default:
1232
                // Transient properties are ignored on purpose here! =)
1233 30
                break;
1234
        }
1235
1236 419
        $this->addDeclaredProperty($property);
1237 419
    }
1238
1239
    /**
1240
     * INTERNAL:
1241
     * Adds a property mapping without completing/validating it.
1242
     * This is mainly used to add inherited property mappings to derived classes.
1243
     */
1244 98
    public function addInheritedProperty(Property $property)
1245
    {
1246 98
        $inheritedProperty = clone $property;
1247 98
        $declaringClass    = $property->getDeclaringClass();
1248
1249 98
        if ($inheritedProperty instanceof FieldMetadata) {
1250 97
            if (! $declaringClass->isMappedSuperclass) {
1251 75
                $inheritedProperty->setTableName($property->getTableName());
0 ignored issues
show
Bug introduced by
The method getTableName() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1251
                $inheritedProperty->setTableName($property->/** @scrutinizer ignore-call */ getTableName());
Loading history...
1252
            }
1253
1254 97
            $this->fieldNames[$property->getColumnName()] = $property->getName();
1255 43
        } elseif ($inheritedProperty instanceof AssociationMetadata) {
1256 42
            if ($declaringClass->isMappedSuperclass) {
1257 10
                $inheritedProperty->setSourceEntity($this->className);
1258
            }
1259
1260
            // Need to add inherited fieldNames
1261 42
            if ($inheritedProperty instanceof ToOneAssociationMetadata && $inheritedProperty->isOwningSide()) {
1262 35
                foreach ($inheritedProperty->getJoinColumns() as $joinColumn) {
1263
                    /** @var JoinColumnMetadata $joinColumn */
1264 34
                    $this->fieldNames[$joinColumn->getColumnName()] = $property->getName();
1265
                }
1266
            }
1267
        }
1268
1269 98
        if (isset($this->declaredProperties[$property->getName()])) {
1270 1
            throw MappingException::duplicateProperty($this->className, $this->getProperty($property->getName()));
1271
        }
1272
1273 98
        $this->declaredProperties[$property->getName()] = $inheritedProperty;
1274
1275 98
        if ($inheritedProperty instanceof VersionFieldMetadata) {
1276 4
            $this->versionProperty = $inheritedProperty;
1277
        }
1278 98
    }
1279
1280
    /**
1281
     * INTERNAL:
1282
     * Adds a sql result set mapping to this class.
1283
     *
1284
     * @param mixed[] $resultMapping
1285
     *
1286
     * @throws MappingException
1287
     */
1288
    public function addSqlResultSetMapping(array $resultMapping)
1289
    {
1290
        if (! isset($resultMapping['name'])) {
1291
            throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->className);
1292
        }
1293
1294
        if (isset($this->sqlResultSetMappings[$resultMapping['name']])) {
1295
            throw MappingException::duplicateResultSetMapping($this->className, $resultMapping['name']);
1296
        }
1297
1298
        if (isset($resultMapping['entities'])) {
1299
            foreach ($resultMapping['entities'] as $key => $entityResult) {
1300
                if (! isset($entityResult['entityClass'])) {
1301
                    throw MappingException::missingResultSetMappingEntity($this->className, $resultMapping['name']);
1302
                }
1303
1304
                $entityClassName                                = $entityResult['entityClass'];
1305
                $resultMapping['entities'][$key]['entityClass'] = $entityClassName;
1306
1307
                if (isset($entityResult['fields'])) {
1308
                    foreach ($entityResult['fields'] as $k => $field) {
1309
                        if (! isset($field['name'])) {
1310
                            throw MappingException::missingResultSetMappingFieldName($this->className, $resultMapping['name']);
1311
                        }
1312
1313
                        if (! isset($field['column'])) {
1314
                            $fieldName = $field['name'];
1315
1316
                            if (strpos($fieldName, '.')) {
1317
                                list(, $fieldName) = explode('.', $fieldName);
1318
                            }
1319
1320
                            $resultMapping['entities'][$key]['fields'][$k]['column'] = $fieldName;
1321
                        }
1322
                    }
1323
                }
1324
            }
1325
        }
1326
1327
        $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping;
0 ignored issues
show
Bug Best Practice introduced by
The property sqlResultSetMappings does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1328
    }
1329
1330
    /**
1331
     * Registers a custom repository class for the entity class.
1332
     *
1333
     * @param string|null $repositoryClassName The class name of the custom mapper.
1334
     */
1335 31
    public function setCustomRepositoryClassName(?string $repositoryClassName)
1336
    {
1337 31
        $this->customRepositoryClassName = $repositoryClassName;
1338 31
    }
1339
1340 164
    public function getCustomRepositoryClassName() : ?string
1341
    {
1342 164
        return $this->customRepositoryClassName;
1343
    }
1344
1345
    /**
1346
     * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
1347
     *
1348
     * @param string $lifecycleEvent
1349
     *
1350
     * @return bool
1351
     */
1352
    public function hasLifecycleCallbacks($lifecycleEvent)
1353
    {
1354
        return isset($this->lifecycleCallbacks[$lifecycleEvent]);
1355
    }
1356
1357
    /**
1358
     * Gets the registered lifecycle callbacks for an event.
1359
     *
1360
     * @param string $event
1361
     *
1362
     * @return string[]
1363
     */
1364
    public function getLifecycleCallbacks($event)
1365
    {
1366
        return $this->lifecycleCallbacks[$event] ?? [];
1367
    }
1368
1369
    /**
1370
     * Adds a lifecycle callback for entities of this class.
1371
     *
1372
     * @param string $callback
1373
     * @param string $event
1374
     */
1375 17
    public function addLifecycleCallback($callback, $event)
1376
    {
1377 17
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
1378 3
            return;
1379
        }
1380
1381 17
        $this->lifecycleCallbacks[$event][] = $callback;
1382 17
    }
1383
1384
    /**
1385
     * Sets the lifecycle callbacks for entities of this class.
1386
     * Any previously registered callbacks are overwritten.
1387
     *
1388
     * @param string[][] $callbacks
1389
     */
1390 98
    public function setLifecycleCallbacks(array $callbacks) : void
1391
    {
1392 98
        $this->lifecycleCallbacks = $callbacks;
1393 98
    }
1394
1395
    /**
1396
     * Adds a entity listener for entities of this class.
1397
     *
1398
     * @param string $eventName The entity lifecycle event.
1399
     * @param string $class     The listener class.
1400
     * @param string $method    The listener callback method.
1401
     *
1402
     * @throws MappingException
1403
     */
1404 17
    public function addEntityListener(string $eventName, string $class, string $method) : void
1405
    {
1406
        $listener = [
1407 17
            'class'  => $class,
1408 17
            'method' => $method,
1409
        ];
1410
1411 17
        if (! class_exists($class)) {
1412 1
            throw MappingException::entityListenerClassNotFound($class, $this->className);
1413
        }
1414
1415 16
        if (! method_exists($class, $method)) {
1416 1
            throw MappingException::entityListenerMethodNotFound($class, $method, $this->className);
1417
        }
1418
1419 15
        if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
1420 1
            throw MappingException::duplicateEntityListener($class, $method, $this->className);
1421
        }
1422
1423 15
        $this->entityListeners[$eventName][] = $listener;
1424 15
    }
1425
1426
    /**
1427
     * Sets the discriminator column definition.
1428
     *
1429
     * @throws MappingException
1430
     *
1431
     * @see getDiscriminatorColumn()
1432
     */
1433 94
    public function setDiscriminatorColumn(DiscriminatorColumnMetadata $discriminatorColumn) : void
1434
    {
1435 94
        if (isset($this->fieldNames[$discriminatorColumn->getColumnName()])) {
1436 1
            throw MappingException::duplicateColumnName($this->className, $discriminatorColumn->getColumnName());
1437
        }
1438
1439 93
        $discriminatorColumn->setTableName($discriminatorColumn->getTableName() ?? $this->getTableName());
1440
1441 93
        $allowedTypeList = ['boolean', 'array', 'object', 'datetime', 'time', 'date'];
1442
1443 93
        if (in_array($discriminatorColumn->getTypeName(), $allowedTypeList, true)) {
1444
            throw MappingException::invalidDiscriminatorColumnType($discriminatorColumn->getTypeName());
1445
        }
1446
1447 93
        $this->discriminatorColumn = $discriminatorColumn;
1448 93
    }
1449
1450
    /**
1451
     * Sets the discriminator values used by this class.
1452
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
1453
     *
1454
     * @param string[] $map
1455
     *
1456
     * @throws MappingException
1457
     */
1458 91
    public function setDiscriminatorMap(array $map) : void
1459
    {
1460 91
        foreach ($map as $value => $className) {
1461 91
            $this->addDiscriminatorMapClass($value, $className);
1462
        }
1463 91
    }
1464
1465
    /**
1466
     * Adds one entry of the discriminator map with a new class and corresponding name.
1467
     *
1468
     * @param string|int $name
1469
     *
1470
     * @throws MappingException
1471
     */
1472 91
    public function addDiscriminatorMapClass($name, string $className) : void
1473
    {
1474 91
        $this->discriminatorMap[$name] = $className;
1475
1476 91
        if ($this->className === $className) {
1477 77
            $this->discriminatorValue = $name;
1478
1479 77
            return;
1480
        }
1481
1482 90
        if (! (class_exists($className) || interface_exists($className))) {
1483
            throw MappingException::invalidClassInDiscriminatorMap($className, $this->className);
1484
        }
1485
1486 90
        if (is_subclass_of($className, $this->className) && ! in_array($className, $this->subClasses, true)) {
1487 85
            $this->subClasses[] = $className;
1488
        }
1489 90
    }
1490
1491 1019
    public function getValueGenerationPlan() : ValueGenerationPlan
1492
    {
1493 1019
        return $this->valueGenerationPlan;
1494
    }
1495
1496 360
    public function setValueGenerationPlan(ValueGenerationPlan $valueGenerationPlan) : void
1497
    {
1498 360
        $this->valueGenerationPlan = $valueGenerationPlan;
1499 360
    }
1500
1501
    /**
1502
     * Checks whether the class has a named native query with the given query name.
1503
     *
1504
     * @param string $name
1505
     */
1506
    public function hasSqlResultSetMapping($name) : bool
1507
    {
1508
        return isset($this->sqlResultSetMappings[$name]);
1509
    }
1510
1511
    /**
1512
     * Marks this class as read only, no change tracking is applied to it.
1513
     */
1514 2
    public function asReadOnly() : void
1515
    {
1516 2
        $this->readOnly = true;
1517 2
    }
1518
1519
    /**
1520
     * Whether this class is read only or not.
1521
     */
1522 441
    public function isReadOnly() : bool
1523
    {
1524 441
        return $this->readOnly;
1525
    }
1526
1527 1079
    public function isVersioned() : bool
1528
    {
1529 1079
        return $this->versionProperty !== null;
1530
    }
1531
1532
    /**
1533
     * Map Embedded Class
1534
     *
1535
     * @param mixed[] $mapping
1536
     *
1537
     * @throws MappingException
1538
     */
1539
    public function mapEmbedded(array $mapping) : void
0 ignored issues
show
Unused Code introduced by
The parameter $mapping is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1539
    public function mapEmbedded(/** @scrutinizer ignore-unused */ array $mapping) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1540
    {
1541
        /*if (isset($this->declaredProperties[$mapping['fieldName']])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1542
            throw MappingException::duplicateProperty($this->className, $this->getProperty($mapping['fieldName']));
1543
        }
1544
1545
        $this->embeddedClasses[$mapping['fieldName']] = [
1546
            'class'          => $this->fullyQualifiedClassName($mapping['class']),
1547
            'columnPrefix'   => $mapping['columnPrefix'],
1548
            'declaredField'  => $mapping['declaredField'] ?? null,
1549
            'originalField'  => $mapping['originalField'] ?? null,
1550
            'declaringClass' => $this,
1551
        ];*/
1552
    }
1553
1554
    /**
1555
     * Inline the embeddable class
1556
     *
1557
     * @param string $property
1558
     */
1559
    public function inlineEmbeddable($property, ClassMetadata $embeddable) : void
0 ignored issues
show
Unused Code introduced by
The parameter $embeddable is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1559
    public function inlineEmbeddable($property, /** @scrutinizer ignore-unused */ ClassMetadata $embeddable) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $property is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1559
    public function inlineEmbeddable(/** @scrutinizer ignore-unused */ $property, ClassMetadata $embeddable) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1560
    {
1561
        /*foreach ($embeddable->fieldMappings as $fieldName => $fieldMapping) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1562
            $fieldMapping['fieldName']     = $property . "." . $fieldName;
1563
            $fieldMapping['originalClass'] = $fieldMapping['originalClass'] ?? $embeddable->getClassName();
1564
            $fieldMapping['originalField'] = $fieldMapping['originalField'] ?? $fieldName;
1565
            $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
1566
                ? $property . '.' . $fieldMapping['declaredField']
1567
                : $property;
1568
1569
            if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
1570
                $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
1571
            } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
1572
                $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName(
1573
                    $property,
1574
                    $fieldMapping['columnName'],
1575
                    $this->reflectionClass->getName(),
1576
                    $embeddable->reflectionClass->getName()
1577
                );
1578
            }
1579
1580
            $this->mapField($fieldMapping);
1581
        }*/
1582
    }
1583
}
1584