Failed Conditions
Pull Request — master (#7095)
by Benjamin
15:14
created

ClassMetadata::setTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.4285
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 458
    public function __construct(
195
        string $entityName,
196
        ClassMetadataBuildingContext $metadataBuildingContext
197
    ) {
198 458
        parent::__construct($entityName, $metadataBuildingContext);
199
200 458
        $this->namingStrategy = $metadataBuildingContext->getNamingStrategy();
201 458
    }
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 1256
    public function getRootClassName() : string
236
    {
237 1256
        return ($this->parent instanceof ClassMetadata && ! $this->parent->isMappedSuperclass)
238 403
            ? $this->parent->getRootClassName()
239 1256
            : $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 1625
    public function wakeupReflection(ReflectionService $reflectionService) : void
349
    {
350
        // Restore ReflectionClass and properties
351 1625
        $this->reflectionClass = $reflectionService->getClass($this->className);
352
353 1625
        if (! $this->reflectionClass) {
354
            return;
355
        }
356
357 1625
        $this->className = $this->reflectionClass->getName();
358
359 1625
        foreach ($this->declaredProperties as $property) {
360
            /** @var Property $property */
361 1624
            $property->wakeupReflection($reflectionService);
362
        }
363 1625
    }
364
365
    /**
366
     * Validates Identifier.
367
     *
368
     * @throws MappingException
369
     */
370 362
    public function validateIdentifier() : void
371
    {
372 362
        if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
373 28
            return;
374
        }
375
376
        // Verify & complete identifier mapping
377 362
        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 358
        $explicitlyGeneratedProperties = array_filter($this->declaredProperties, function (Property $property) : bool {
382 358
            return $property instanceof FieldMetadata
383 358
                && $property->isPrimaryKey()
384 358
                && $property->hasValueGenerator();
385 358
        });
386
387 358
        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 358
    }
391
392
    /**
393
     * Validates association targets actually exist.
394
     *
395
     * @throws MappingException
396
     */
397 361
    public function validateAssociations() : void
398
    {
399 361
        array_map(
400 361
            function (Property $property) {
401 361
                if (! ($property instanceof AssociationMetadata)) {
402 358
                    return;
403
                }
404
405 248
                $targetEntity = $property->getTargetEntity();
406
407 248
                if (! class_exists($targetEntity)) {
408 1
                    throw MappingException::invalidTargetEntityClass($targetEntity, $this->className, $property->getName());
409
                }
410 361
            },
411 361
            $this->declaredProperties
412
        );
413 360
    }
414
415
    /**
416
     * Validates lifecycle callbacks.
417
     *
418
     * @throws MappingException
419
     */
420 361
    public function validateLifecycleCallbacks(ReflectionService $reflectionService) : void
421
    {
422 361
        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 360
    }
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 1019
    public function isIdentifier(string $fieldName) : bool
448
    {
449 1019
        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 1018
        if (! $this->isIdentifierComposite()) {
454 1014
            return $fieldName === $this->identifier[0];
455
        }
456
457 92
        return in_array($fieldName, $this->identifier, true);
458
    }
459
460 1203
    public function isIdentifierComposite() : bool
461
    {
462 1203
        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 406
    protected function validateAndCompleteFieldMapping(FieldMetadata $property)
501
    {
502 406
        $fieldName  = $property->getName();
503 406
        $columnName = $property->getColumnName();
504
505 406
        if (empty($columnName)) {
506 342
            $columnName = $this->namingStrategy->propertyToColumnName($fieldName, $this->className);
507
508 342
            $property->setColumnName($columnName);
509
        }
510
511 406
        if (! $this->isMappedSuperclass) {
512 399
            $property->setTableName($this->getTableName());
513
        }
514
515
        // Check for already declared column
516 406
        if (isset($this->fieldNames[$columnName]) ||
517 406
            ($this->discriminatorColumn !== null && $this->discriminatorColumn->getColumnName() === $columnName)) {
518 2
            throw MappingException::duplicateColumnName($this->className, $columnName);
519
        }
520
521
        // Complete id mapping
522 405
        if ($property->isPrimaryKey()) {
523 390
            if ($this->versionProperty !== null && $this->versionProperty->getName() === $fieldName) {
524
                throw MappingException::cannotVersionIdField($this->className, $fieldName);
525
            }
526
527 390
            if ($property->getType()->canRequireSQLConversion()) {
528
                throw MappingException::sqlConversionNotAllowedForPrimaryKeyProperties($this->className, $property);
529
            }
530
531 390
            if (! in_array($fieldName, $this->identifier, true)) {
532 390
                $this->identifier[] = $fieldName;
533
            }
534
        }
535
536 405
        $this->fieldNames[$columnName] = $fieldName;
537 405
    }
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 286
    protected function validateAndCompleteAssociationMapping(AssociationMetadata $property)
577
    {
578 286
        $fieldName    = $property->getName();
579 286
        $targetEntity = $property->getTargetEntity();
580
581 286
        if (! $targetEntity) {
582
            throw MappingException::missingTargetEntity($fieldName);
583
        }
584
585 286
        $property->setSourceEntity($this->className);
586 286
        $property->setTargetEntity($targetEntity);
587
588
        // Complete id mapping
589 286
        if ($property->isPrimaryKey()) {
590 46
            if ($property->isOrphanRemoval()) {
591 1
                throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->className, $fieldName);
592
            }
593
594 45
            if (! in_array($property->getName(), $this->identifier, true)) {
595 45
                if ($property instanceof ToOneAssociationMetadata && count($property->getJoinColumns()) >= 2) {
596
                    throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
597
                        $property->getTargetEntity(),
598
                        $this->className,
599
                        $fieldName
600
                    );
601
                }
602
603 45
                $this->identifier[] = $property->getName();
604
            }
605
606 45
            if ($this->cache && ! $property->getCache()) {
607 2
                throw CacheException::nonCacheableEntityAssociation($this->className, $fieldName);
608
            }
609
610 43
            if ($property instanceof ToManyAssociationMetadata) {
611 1
                throw MappingException::illegalToManyIdentifierAssociation($this->className, $property->getName());
612
            }
613
        }
614
615
        // Cascades
616 282
        $cascadeTypes = ['remove', 'persist', 'refresh'];
617 282
        $cascades     = array_map('strtolower', $property->getCascade());
618
619 282
        if (in_array('all', $cascades, true)) {
620 6
            $cascades = $cascadeTypes;
621
        }
622
623 282
        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 281
        $property->setCascade($cascades);
630 281
    }
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 246
    protected function validateAndCompleteToOneAssociationMetadata(ToOneAssociationMetadata $property)
641
    {
642 246
        $fieldName = $property->getName();
643
644 246
        if ($property->isOwningSide()) {
645 242
            if (empty($property->getJoinColumns())) {
646
                // Apply default join column
647 86
                $property->addJoinColumn(new JoinColumnMetadata());
648
            }
649
650 242
            $uniqueConstraintColumns = [];
651
652 242
            foreach ($property->getJoinColumns() as $joinColumn) {
653
                /** @var JoinColumnMetadata $joinColumn */
654 242
                if ($property instanceof OneToOneAssociationMetadata && $this->inheritanceType !== InheritanceType::SINGLE_TABLE) {
655 111
                    if (count($property->getJoinColumns()) === 1) {
656 109
                        if (! $property->isPrimaryKey()) {
657 109
                            $joinColumn->setUnique(true);
658
                        }
659
                    } else {
660 2
                        $uniqueConstraintColumns[] = $joinColumn->getColumnName();
661
                    }
662
                }
663
664 242
                $joinColumn->setTableName(! $this->isMappedSuperclass ? $this->getTableName() : null);
665
666 242
                if (! $joinColumn->getColumnName()) {
667 99
                    $joinColumn->setColumnName($this->namingStrategy->joinColumnName($fieldName, $this->className));
668
                }
669
670 242
                if (! $joinColumn->getReferencedColumnName()) {
671 86
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
672
                }
673
674 242
                $this->fieldNames[$joinColumn->getColumnName()] = $fieldName;
675
            }
676
677 242
            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 246
        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 246
        if ($property->isPrimaryKey() && ! $property->isOwningSide()) {
710 1
            throw MappingException::illegalInverseIdentifierAssociation($this->className, $fieldName);
711
        }
712 245
    }
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 171
    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 171
    }
725
726
    /**
727
     * Validates & completes a one-to-one association mapping.
728
     *
729
     * @param OneToOneAssociationMetadata $property The association mapping to validate & complete.
730
     */
731 128
    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 128
    }
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 146
    protected function validateAndCompleteManyToOneMapping(ManyToOneAssociationMetadata $property)
744
    {
745
        // A many-to-one mapping is essentially a one-one backreference
746 146
        if ($property->isOrphanRemoval()) {
747
            throw MappingException::illegalOrphanRemoval($this->className, $property->getName());
748
        }
749 146
    }
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 115
    protected function validateAndCompleteOneToManyMapping(OneToManyAssociationMetadata $property)
759
    {
760
        // OneToMany MUST have mappedBy
761 115
        if (! $property->getMappedBy()) {
762
            throw MappingException::oneToManyRequiresMappedBy($property->getName());
763
        }
764
765 115
        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 115
    }
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 395
    public function getIdentifierFieldNames()
871
    {
872 395
        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 1044
    public function getIdentifier()
912
    {
913 1044
        return $this->identifier;
914
    }
915
916
    /**
917
     * {@inheritDoc}
918
     */
919 186
    public function hasField($fieldName)
920
    {
921 186
        return isset($this->declaredProperties[$fieldName])
922 186
            && $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 1582
    public function getTableName() : ?string
978
    {
979 1582
        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 1075
    public function getSubClasses() : array
1022
    {
1023 1075
        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 620
    public function isInheritedProperty($fieldName)
1135
    {
1136 620
        $declaringClass = $this->declaredProperties[$fieldName]->getDeclaringClass();
1137
1138 620
        return ! ($declaringClass->className === $this->className);
1139
    }
1140
1141
    /**
1142
     * {@inheritdoc}
1143
     */
1144 440
    public function setTable(TableMetadata $table) : void
1145
    {
1146 440
        $this->table = $table;
1147
1148 440
        if (empty($table->getName())) {
1149
            $table->setName($this->namingStrategy->classToTableName($this->className));
1150
        }
1151 440
    }
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 909
    public function getColumn(string $columnName) : ?LocalColumnMetadata
1169
    {
1170 909
        foreach ($this->declaredProperties as $property) {
1171 909
            if ($property instanceof LocalColumnMetadata && $property->getColumnName() === $columnName) {
1172 909
                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 430
    public function addProperty(Property $property)
1187
    {
1188 430
        $fieldName = $property->getName();
1189
1190
        // Check for empty field name
1191 430
        if (empty($fieldName)) {
1192 1
            throw MappingException::missingFieldName($this->className);
1193
        }
1194
1195 429
        $property->setDeclaringClass($this);
1196
1197
        switch (true) {
1198 429
            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 428
            case ($property instanceof FieldMetadata):
1204 405
                $this->validateAndCompleteFieldMapping($property);
1205 404
                break;
1206
1207 289
            case ($property instanceof OneToOneAssociationMetadata):
1208 130
                $this->validateAndCompleteAssociationMapping($property);
1209 129
                $this->validateAndCompleteToOneAssociationMetadata($property);
1210 128
                $this->validateAndCompleteOneToOneMapping($property);
1211 128
                break;
1212
1213 225
            case ($property instanceof OneToManyAssociationMetadata):
1214 115
                $this->validateAndCompleteAssociationMapping($property);
1215 115
                $this->validateAndCompleteToManyAssociationMetadata($property);
1216 115
                $this->validateAndCompleteOneToManyMapping($property);
1217 115
                break;
1218
1219 221
            case ($property instanceof ManyToOneAssociationMetadata):
1220 149
                $this->validateAndCompleteAssociationMapping($property);
1221 146
                $this->validateAndCompleteToOneAssociationMetadata($property);
1222 146
                $this->validateAndCompleteManyToOneMapping($property);
1223 146
                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 421
        $this->addDeclaredProperty($property);
1237 421
    }
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 1023
    public function getValueGenerationPlan() : ValueGenerationPlan
1492
    {
1493 1023
        return $this->valueGenerationPlan;
1494
    }
1495
1496 362
    public function setValueGenerationPlan(ValueGenerationPlan $valueGenerationPlan) : void
1497
    {
1498 362
        $this->valueGenerationPlan = $valueGenerationPlan;
1499 362
    }
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 443
    public function isReadOnly() : bool
1523
    {
1524 443
        return $this->readOnly;
1525
    }
1526
1527 1083
    public function isVersioned() : bool
1528
    {
1529 1083
        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