Failed Conditions
Pull Request — master (#7085)
by Guilherme
14:34
created

ClassMetadata::setSubclasses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 2
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 named native queries allowed to be called directly from Repository.
86
     *
87
     * A native SQL named query definition has the following structure:
88
     * <pre>
89
     * array(
90
     *     'name'               => <query name>,
91
     *     'query'              => <sql query>,
92
     *     'resultClass'        => <class of the result>,
93
     *     'resultSetMapping'   => <name of a SqlResultSetMapping>
94
     * )
95
     * </pre>
96
     *
97
     * @var string[][]
98
     */
99
    public $namedNativeQueries = [];
100
101
    /**
102
     * READ-ONLY: The mappings of the results of native SQL queries.
103
     *
104
     * A native result mapping definition has the following structure:
105
     * <pre>
106
     * array(
107
     *     'name'               => <result name>,
108
     *     'entities'           => array(<entity result mapping>),
109
     *     'columns'            => array(<column result mapping>)
110
     * )
111
     * </pre>
112
     *
113
     * @var mixed[][]
114
     */
115
    public $sqlResultSetMappings = [];
116
117
    /**
118
     * READ-ONLY: The registered lifecycle callbacks for entities of this class.
119
     *
120
     * @var string[][]
121
     */
122
    public $lifecycleCallbacks = [];
123
124
    /**
125
     * READ-ONLY: The registered entity listeners.
126
     *
127
     * @var mixed[][]
128
     */
129
    public $entityListeners = [];
130
131
    /**
132
     * READ-ONLY: The field names of all fields that are part of the identifier/primary key
133
     * of the mapped entity class.
134
     *
135
     * @var string[]
136
     */
137
    public $identifier = [];
138
139
    /**
140
     * READ-ONLY: The inheritance mapping type used by the class.
141
     *
142
     * @var string
143
     */
144
    public $inheritanceType = InheritanceType::NONE;
145
146
    /**
147
     * READ-ONLY: The policy used for change-tracking on entities of this class.
148
     *
149
     * @var string
150
     */
151
    public $changeTrackingPolicy = ChangeTrackingPolicy::DEFERRED_IMPLICIT;
152
153
    /**
154
     * READ-ONLY: The discriminator value of this class.
155
     *
156
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
157
     * where a discriminator column is used.</b>
158
     *
159
     * @var mixed
160
     *
161
     * @see discriminatorColumn
162
     */
163
    public $discriminatorValue;
164
165
    /**
166
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
167
     *
168
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
169
     * where a discriminator column is used.</b>
170
     *
171
     * @var string[]
172
     *
173
     * @see discriminatorColumn
174
     */
175
    public $discriminatorMap = [];
176
177
    /**
178
     * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
179
     * inheritance mappings.
180
     *
181
     * @var DiscriminatorColumnMetadata
182
     */
183
    public $discriminatorColumn;
184
185
    /**
186
     * READ-ONLY: The primary table metadata.
187
     *
188
     * @var TableMetadata
189
     */
190
    public $table;
191
192
    /**
193
     * READ-ONLY: An array of field names. Used to look up field names from column names.
194
     * Keys are column names and values are field names.
195
     *
196
     * @var string[]
197
     */
198
    public $fieldNames = [];
199
200
    /**
201
     * READ-ONLY: The field which is used for versioning in optimistic locking (if any).
202
     *
203
     * @var FieldMetadata|null
204
     */
205
    public $versionProperty;
206
207
    /**
208
     * NamingStrategy determining the default column and table names.
209
     *
210
     * @var NamingStrategy
211
     */
212
    protected $namingStrategy;
213
214
    /**
215
     * Value generation plan is responsible for generating values for auto-generated fields.
216
     *
217
     * @var ValueGenerationPlan
218
     */
219
    protected $valueGenerationPlan;
220
221
    /**
222
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
223
     * metadata of the class with the given name.
224
     *
225
     * @param string $entityName The name of the entity class.
226
     */
227 470
    public function __construct(
228
        string $entityName,
229
        ClassMetadataBuildingContext $metadataBuildingContext
230
    ) {
231 470
        parent::__construct($entityName, $metadataBuildingContext);
232
233 470
        $this->namingStrategy = $metadataBuildingContext->getNamingStrategy();
234 470
    }
235
236 2
    public function setClassName(string $className)
237
    {
238 2
        $this->className = $className;
239 2
    }
240
241
    public function getColumnsIterator() : \ArrayIterator
242
    {
243
        $iterator = parent::getColumnsIterator();
244
245
        if ($this->discriminatorColumn) {
246
            $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

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

792
    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...
793
    {
794
        // Do nothing
795 172
    }
796
797
    /**
798
     * Validates & completes a one-to-one association mapping.
799
     *
800
     * @param OneToOneAssociationMetadata $property The association mapping to validate & complete.
801
     */
802 130
    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

802
    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...
803
    {
804
        // Do nothing
805 130
    }
806
807
    /**
808
     * Validates & completes a many-to-one association mapping.
809
     *
810
     * @param ManyToOneAssociationMetadata $property The association mapping to validate & complete.
811
     *
812
     * @throws MappingException
813
     */
814 145
    protected function validateAndCompleteManyToOneMapping(ManyToOneAssociationMetadata $property)
815
    {
816
        // A many-to-one mapping is essentially a one-one backreference
817 145
        if ($property->isOrphanRemoval()) {
818
            throw MappingException::illegalOrphanRemoval($this->className, $property->getName());
819
        }
820 145
    }
821
822
    /**
823
     * Validates & completes a one-to-many association mapping.
824
     *
825
     * @param OneToManyAssociationMetadata $property The association mapping to validate & complete.
826
     *
827
     * @throws MappingException
828
     */
829 116
    protected function validateAndCompleteOneToManyMapping(OneToManyAssociationMetadata $property)
830
    {
831
        // OneToMany MUST have mappedBy
832 116
        if (! $property->getMappedBy()) {
833
            throw MappingException::oneToManyRequiresMappedBy($property->getName());
834
        }
835
836 116
        if ($property->isOrphanRemoval()) {
837 21
            $cascades = $property->getCascade();
838
839 21
            if (! in_array('remove', $cascades, true)) {
840 18
                $cascades[] = 'remove';
841
842 18
                $property->setCascade($cascades);
843
            }
844
        }
845 116
    }
846
847
    /**
848
     * Validates & completes a many-to-many association mapping.
849
     *
850
     * @param ManyToManyAssociationMetadata $property The association mapping to validate & complete.
851
     *
852
     * @throws MappingException
853
     */
854 111
    protected function validateAndCompleteManyToManyMapping(ManyToManyAssociationMetadata $property)
855
    {
856 111
        if ($property->isOwningSide()) {
857
            // owning side MUST have a join table
858 99
            $joinTable = $property->getJoinTable() ?: new JoinTableMetadata();
859
860 99
            $property->setJoinTable($joinTable);
861
862 99
            if (! $joinTable->getName()) {
863 18
                $joinTableName = $this->namingStrategy->joinTableName(
864 18
                    $property->getSourceEntity(),
865 18
                    $property->getTargetEntity(),
866 18
                    $property->getName()
867
                );
868
869 18
                $joinTable->setName($joinTableName);
870
            }
871
872 99
            $selfReferencingEntityWithoutJoinColumns = $property->getSourceEntity() === $property->getTargetEntity() && ! $joinTable->hasColumns();
873
874 99
            if (! $joinTable->getJoinColumns()) {
875 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
876 16
                $sourceReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'source' : $referencedColumnName;
877 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getSourceEntity(), $sourceReferenceName);
878 16
                $joinColumn           = new JoinColumnMetadata();
879
880 16
                $joinColumn->setColumnName($columnName);
881 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
882 16
                $joinColumn->setOnDelete('CASCADE');
883
884 16
                $joinTable->addJoinColumn($joinColumn);
885
            }
886
887 99
            if (! $joinTable->getInverseJoinColumns()) {
888 16
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
889 16
                $targetReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'target' : $referencedColumnName;
890 16
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getTargetEntity(), $targetReferenceName);
891 16
                $joinColumn           = new JoinColumnMetadata();
892
893 16
                $joinColumn->setColumnName($columnName);
894 16
                $joinColumn->setReferencedColumnName($referencedColumnName);
895 16
                $joinColumn->setOnDelete('CASCADE');
896
897 16
                $joinTable->addInverseJoinColumn($joinColumn);
898
            }
899
900 99
            foreach ($joinTable->getJoinColumns() as $joinColumn) {
901
                /** @var JoinColumnMetadata $joinColumn */
902 99
                if (! $joinColumn->getReferencedColumnName()) {
903 2
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
904
                }
905
906 99
                $referencedColumnName = $joinColumn->getReferencedColumnName();
907
908 99
                if (! $joinColumn->getColumnName()) {
909 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
910 2
                        $property->getSourceEntity(),
911 2
                        $referencedColumnName
912
                    );
913
914 99
                    $joinColumn->setColumnName($columnName);
915
                }
916
            }
917
918 99
            foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) {
919
                /** @var JoinColumnMetadata $inverseJoinColumn */
920 99
                if (! $inverseJoinColumn->getReferencedColumnName()) {
921 2
                    $inverseJoinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
922
                }
923
924 99
                $referencedColumnName = $inverseJoinColumn->getReferencedColumnName();
925
926 99
                if (! $inverseJoinColumn->getColumnName()) {
927 2
                    $columnName = $this->namingStrategy->joinKeyColumnName(
928 2
                        $property->getTargetEntity(),
929 2
                        $referencedColumnName
930
                    );
931
932 99
                    $inverseJoinColumn->setColumnName($columnName);
933
                }
934
            }
935
        }
936 111
    }
937
938
    /**
939
     * {@inheritDoc}
940
     */
941 395
    public function getIdentifierFieldNames()
942
    {
943 395
        return $this->identifier;
944
    }
945
946
    /**
947
     * Gets the name of the single id field. Note that this only works on
948
     * entity classes that have a single-field pk.
949
     *
950
     * @return string
951
     *
952
     * @throws MappingException If the class has a composite primary key.
953
     */
954 149
    public function getSingleIdentifierFieldName()
955
    {
956 149
        if ($this->isIdentifierComposite()) {
957 1
            throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->className);
958
        }
959
960 148
        if (! isset($this->identifier[0])) {
961 1
            throw MappingException::noIdDefined($this->className);
962
        }
963
964 147
        return $this->identifier[0];
965
    }
966
967
    /**
968
     * INTERNAL:
969
     * Sets the mapped identifier/primary key fields of this class.
970
     * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
971
     *
972
     * @param mixed[] $identifier
973
     */
974 100
    public function setIdentifier(array $identifier)
975
    {
976 100
        $this->identifier = $identifier;
977 100
    }
978
979
    /**
980
     * {@inheritDoc}
981
     */
982 1047
    public function getIdentifier()
983
    {
984 1047
        return $this->identifier;
985
    }
986
987
    /**
988
     * {@inheritDoc}
989
     */
990 184
    public function hasField($fieldName)
991
    {
992 184
        return isset($this->declaredProperties[$fieldName])
993 184
            && $this->declaredProperties[$fieldName] instanceof FieldMetadata;
994
    }
995
996
    /**
997
     * Returns an array with identifier column names and their corresponding ColumnMetadata.
998
     *
999
     * @return ColumnMetadata[]
1000
     */
1001 440
    public function getIdentifierColumns(EntityManagerInterface $em) : array
1002
    {
1003 440
        $columns = [];
1004
1005 440
        foreach ($this->identifier as $idProperty) {
1006 440
            $property = $this->getProperty($idProperty);
1007
1008 440
            if ($property instanceof FieldMetadata) {
1009 435
                $columns[$property->getColumnName()] = $property;
1010
1011 435
                continue;
1012
            }
1013
1014
            /** @var AssociationMetadata $property */
1015
1016
            // Association defined as Id field
1017 24
            $targetClass = $em->getClassMetadata($property->getTargetEntity());
1018
1019 24
            if (! $property->isOwningSide()) {
1020
                $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

1020
                /** @scrutinizer ignore-call */ 
1021
                $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...
1021
                $targetClass = $em->getClassMetadata($property->getTargetEntity());
1022
            }
1023
1024 24
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
1025
                ? $property->getJoinTable()->getInverseJoinColumns()
1026 24
                : $property->getJoinColumns()
1027
            ;
1028
1029 24
            foreach ($joinColumns as $joinColumn) {
1030
                /** @var JoinColumnMetadata $joinColumn */
1031 24
                $columnName           = $joinColumn->getColumnName();
1032 24
                $referencedColumnName = $joinColumn->getReferencedColumnName();
1033
1034 24
                if (! $joinColumn->getType()) {
1035 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

1035
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $em));
Loading history...
1036
                }
1037
1038 24
                $columns[$columnName] = $joinColumn;
1039
            }
1040
        }
1041
1042 440
        return $columns;
1043
    }
1044
1045
    /**
1046
     * Gets the name of the primary table.
1047
     */
1048 1590
    public function getTableName() : ?string
1049
    {
1050 1590
        return $this->table->getName();
1051
    }
1052
1053
    /**
1054
     * Gets primary table's schema name.
1055
     */
1056 14
    public function getSchemaName() : ?string
1057
    {
1058 14
        return $this->table->getSchema();
1059
    }
1060
1061
    /**
1062
     * Gets the table name to use for temporary identifier tables of this class.
1063
     */
1064 7
    public function getTemporaryIdTableName() : string
1065
    {
1066 7
        $schema = $this->getSchemaName() === null
1067 6
            ? ''
1068 7
            : $this->getSchemaName() . '_'
1069
        ;
1070
1071
        // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
1072 7
        return $schema . $this->getTableName() . '_id_tmp';
1073
    }
1074
1075
    /**
1076
     * Sets the mapped subclasses of this class.
1077
     *
1078
     * @todo guilhermeblanco Only used for ClassMetadataTest. Remove if possible!
1079
     *
1080
     * @param string[] $subclasses The names of all mapped subclasses.
1081
     */
1082 4
    public function setSubclasses(array $subclasses) : void
1083
    {
1084 4
        foreach ($subclasses as $subclass) {
1085 3
            $this->subClasses[] = $subclass;
1086
        }
1087 4
    }
1088
1089
    /**
1090
     * @return string[]
1091
     */
1092 1078
    public function getSubClasses() : array
1093
    {
1094 1078
        return $this->subClasses;
1095
    }
1096
1097
    /**
1098
     * Sets the inheritance type used by the class and its subclasses.
1099
     *
1100
     * @param int $type
1101
     *
1102
     * @throws MappingException
1103
     */
1104 120
    public function setInheritanceType($type) : void
1105
    {
1106 120
        if (! $this->isInheritanceType($type)) {
1107
            throw MappingException::invalidInheritanceType($this->className, $type);
1108
        }
1109
1110 120
        $this->inheritanceType = $type;
1111 120
    }
1112
1113
    /**
1114
     * Sets the override property mapping for an entity relationship.
1115
     *
1116
     * @throws \RuntimeException
1117
     * @throws MappingException
1118
     * @throws CacheException
1119
     */
1120 12
    public function setPropertyOverride(Property $property) : void
1121
    {
1122 12
        $fieldName = $property->getName();
1123
1124 12
        if (! isset($this->declaredProperties[$fieldName])) {
1125 2
            throw MappingException::invalidOverrideFieldName($this->className, $fieldName);
1126
        }
1127
1128 10
        $originalProperty          = $this->getProperty($fieldName);
1129 10
        $originalPropertyClassName = get_class($originalProperty);
1130
1131
        // If moving from transient to persistent, assume it's a new property
1132 10
        if ($originalPropertyClassName === TransientMetadata::class) {
1133 1
            unset($this->declaredProperties[$fieldName]);
1134
1135 1
            $this->addProperty($property);
1136
1137 1
            return;
1138
        }
1139
1140
        // Do not allow to change property type
1141 9
        if ($originalPropertyClassName !== get_class($property)) {
1142
            throw MappingException::invalidOverridePropertyType($this->className, $fieldName);
1143
        }
1144
1145
        // Do not allow to change version property
1146 9
        if ($originalProperty instanceof VersionFieldMetadata) {
1147
            throw MappingException::invalidOverrideVersionField($this->className, $fieldName);
1148
        }
1149
1150 9
        unset($this->declaredProperties[$fieldName]);
1151
1152 9
        if ($property instanceof FieldMetadata) {
1153
            // Unset defined fieldName prior to override
1154 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

1154
            unset($this->fieldNames[$originalProperty->/** @scrutinizer ignore-call */ getColumnName()]);
Loading history...
1155
1156
            // Revert what should not be allowed to change
1157 5
            $property->setDeclaringClass($originalProperty->getDeclaringClass());
1158 5
            $property->setPrimaryKey($originalProperty->isPrimaryKey());
1159 9
        } elseif ($property instanceof AssociationMetadata) {
1160
            // Unset all defined fieldNames prior to override
1161 9
            if ($originalProperty instanceof ToOneAssociationMetadata && $originalProperty->isOwningSide()) {
1162 5
                foreach ($originalProperty->getJoinColumns() as $joinColumn) {
1163 5
                    unset($this->fieldNames[$joinColumn->getColumnName()]);
1164
                }
1165
            }
1166
1167
            // Override what it should be allowed to change
1168 9
            if ($property->getInversedBy()) {
1169 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

1169
                $originalProperty->/** @scrutinizer ignore-call */ 
1170
                                   setInversedBy($property->getInversedBy());
Loading history...
1170
            }
1171
1172 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

1172
            if ($property->getFetchMode() !== $originalProperty->/** @scrutinizer ignore-call */ getFetchMode()) {
Loading history...
1173 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

1173
                $originalProperty->/** @scrutinizer ignore-call */ 
1174
                                   setFetchMode($property->getFetchMode());
Loading history...
1174
            }
1175
1176 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

1176
            if ($originalProperty instanceof ToOneAssociationMetadata && $property->/** @scrutinizer ignore-call */ getJoinColumns()) {
Loading history...
1177 5
                $originalProperty->setJoinColumns($property->getJoinColumns());
1178 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

1178
            } elseif ($originalProperty instanceof ManyToManyAssociationMetadata && $property->/** @scrutinizer ignore-call */ getJoinTable()) {
Loading history...
1179 4
                $originalProperty->setJoinTable($property->getJoinTable());
1180
            }
1181
1182 9
            $property = $originalProperty;
1183
        }
1184
1185 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

1185
        $this->addProperty(/** @scrutinizer ignore-type */ $property);
Loading history...
1186 9
    }
1187
1188
    /**
1189
     * Checks if this entity is the root in any entity-inheritance-hierarchy.
1190
     *
1191
     * @return bool
1192
     */
1193 338
    public function isRootEntity()
1194
    {
1195 338
        return $this->className === $this->getRootClassName();
1196
    }
1197
1198
    /**
1199
     * Checks whether a mapped field is inherited from a superclass.
1200
     *
1201
     * @param string $fieldName
1202
     *
1203
     * @return bool TRUE if the field is inherited, FALSE otherwise.
1204
     */
1205 622
    public function isInheritedProperty($fieldName)
1206
    {
1207 622
        $declaringClass = $this->declaredProperties[$fieldName]->getDeclaringClass();
1208
1209 622
        return ! ($declaringClass->className === $this->className);
1210
    }
1211
1212
    /**
1213
     * {@inheritdoc}
1214
     */
1215 451
    public function setTable(TableMetadata $table) : void
1216
    {
1217 451
        $this->table = $table;
1218
1219 451
        if (empty($table->getName())) {
1220
            $table->setName($this->namingStrategy->classToTableName($this->className));
1221
        }
1222 451
    }
1223
1224
    /**
1225
     * Checks whether the given type identifies an inheritance type.
1226
     *
1227
     * @param int $type
1228
     *
1229
     * @return bool TRUE if the given type identifies an inheritance type, FALSe otherwise.
1230
     */
1231 120
    private function isInheritanceType($type)
1232
    {
1233 120
        return $type === InheritanceType::NONE
1234 94
            || $type === InheritanceType::SINGLE_TABLE
1235 52
            || $type === InheritanceType::JOINED
1236 120
            || $type === InheritanceType::TABLE_PER_CLASS;
1237
    }
1238
1239 912
    public function getColumn(string $columnName) : ?LocalColumnMetadata
1240
    {
1241 912
        foreach ($this->declaredProperties as $property) {
1242 912
            if ($property instanceof LocalColumnMetadata && $property->getColumnName() === $columnName) {
1243 912
                return $property;
1244
            }
1245
        }
1246
1247
        return null;
1248
    }
1249
1250
    /**
1251
     * Add a property mapping.
1252
     *
1253
     * @throws \RuntimeException
1254
     * @throws MappingException
1255
     * @throws CacheException
1256
     */
1257 432
    public function addProperty(Property $property)
1258
    {
1259 432
        $fieldName = $property->getName();
1260
1261
        // Check for empty field name
1262 432
        if (empty($fieldName)) {
1263 1
            throw MappingException::missingFieldName($this->className);
1264
        }
1265
1266 431
        $property->setDeclaringClass($this);
1267
1268
        switch (true) {
1269 431
            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...
1270 20
                $this->validateAndCompleteFieldMapping($property);
1271 20
                $this->validateAndCompleteVersionFieldMapping($property);
1272 19
                break;
1273
1274 430
            case ($property instanceof FieldMetadata):
1275 407
                $this->validateAndCompleteFieldMapping($property);
1276 406
                break;
1277
1278 291
            case ($property instanceof OneToOneAssociationMetadata):
1279 132
                $this->validateAndCompleteAssociationMapping($property);
1280 131
                $this->validateAndCompleteToOneAssociationMetadata($property);
1281 130
                $this->validateAndCompleteOneToOneMapping($property);
1282 130
                break;
1283
1284 227
            case ($property instanceof OneToManyAssociationMetadata):
1285 116
                $this->validateAndCompleteAssociationMapping($property);
1286 116
                $this->validateAndCompleteToManyAssociationMetadata($property);
1287 116
                $this->validateAndCompleteOneToManyMapping($property);
1288 116
                break;
1289
1290 223
            case ($property instanceof ManyToOneAssociationMetadata):
1291 148
                $this->validateAndCompleteAssociationMapping($property);
1292 145
                $this->validateAndCompleteToOneAssociationMetadata($property);
1293 145
                $this->validateAndCompleteManyToOneMapping($property);
1294 145
                break;
1295
1296 129
            case ($property instanceof ManyToManyAssociationMetadata):
1297 112
                $this->validateAndCompleteAssociationMapping($property);
1298 111
                $this->validateAndCompleteToManyAssociationMetadata($property);
1299 111
                $this->validateAndCompleteManyToManyMapping($property);
1300 111
                break;
1301
1302
            default:
1303
                // Transient properties are ignored on purpose here! =)
1304 32
                break;
1305
        }
1306
1307 423
        $this->addDeclaredProperty($property);
1308 423
    }
1309
1310
    /**
1311
     * INTERNAL:
1312
     * Adds a property mapping without completing/validating it.
1313
     * This is mainly used to add inherited property mappings to derived classes.
1314
     */
1315 98
    public function addInheritedProperty(Property $property)
1316
    {
1317 98
        $inheritedProperty = clone $property;
1318 98
        $declaringClass    = $property->getDeclaringClass();
1319
1320 98
        if ($inheritedProperty instanceof FieldMetadata) {
1321 97
            if (! $declaringClass->isMappedSuperclass) {
1322 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

1322
                $inheritedProperty->setTableName($property->/** @scrutinizer ignore-call */ getTableName());
Loading history...
1323
            }
1324
1325 97
            $this->fieldNames[$property->getColumnName()] = $property->getName();
1326 43
        } elseif ($inheritedProperty instanceof AssociationMetadata) {
1327 42
            if ($declaringClass->isMappedSuperclass) {
1328 10
                $inheritedProperty->setSourceEntity($this->className);
1329
            }
1330
1331
            // Need to add inherited fieldNames
1332 42
            if ($inheritedProperty instanceof ToOneAssociationMetadata && $inheritedProperty->isOwningSide()) {
1333 35
                foreach ($inheritedProperty->getJoinColumns() as $joinColumn) {
1334
                    /** @var JoinColumnMetadata $joinColumn */
1335 34
                    $this->fieldNames[$joinColumn->getColumnName()] = $property->getName();
1336
                }
1337
            }
1338
        }
1339
1340 98
        if (isset($this->declaredProperties[$property->getName()])) {
1341 1
            throw MappingException::duplicateProperty($this->className, $this->getProperty($property->getName()));
1342
        }
1343
1344 98
        $this->declaredProperties[$property->getName()] = $inheritedProperty;
1345
1346 98
        if ($inheritedProperty instanceof VersionFieldMetadata) {
1347 4
            $this->versionProperty = $inheritedProperty;
1348
        }
1349 98
    }
1350
1351
    /**
1352
     * INTERNAL:
1353
     * Adds a named native query to this class.
1354
     *
1355
     * @param mixed[] $queryMapping
1356
     *
1357
     * @throws MappingException
1358
     */
1359 25
    public function addNamedNativeQuery(string $name, string $query, array $queryMapping)
1360
    {
1361 25
        if (isset($this->namedNativeQueries[$name])) {
1362 1
            throw MappingException::duplicateQueryMapping($this->className, $name);
1363
        }
1364
1365 25
        if (! isset($queryMapping['resultClass']) && ! isset($queryMapping['resultSetMapping'])) {
1366
            throw MappingException::missingQueryMapping($this->className, $name);
1367
        }
1368
1369 25
        $this->namedNativeQueries[$name] = array_merge(['query' => $query], $queryMapping);
1370 25
    }
1371
1372
    /**
1373
     * INTERNAL:
1374
     * Adds a sql result set mapping to this class.
1375
     *
1376
     * @param mixed[] $resultMapping
1377
     *
1378
     * @throws MappingException
1379
     */
1380 25
    public function addSqlResultSetMapping(array $resultMapping)
1381
    {
1382 25
        if (! isset($resultMapping['name'])) {
1383
            throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->className);
1384
        }
1385
1386 25
        if (isset($this->sqlResultSetMappings[$resultMapping['name']])) {
1387 1
            throw MappingException::duplicateResultSetMapping($this->className, $resultMapping['name']);
1388
        }
1389
1390 25
        if (isset($resultMapping['entities'])) {
1391 25
            foreach ($resultMapping['entities'] as $key => $entityResult) {
1392 25
                if (! isset($entityResult['entityClass'])) {
1393 1
                    throw MappingException::missingResultSetMappingEntity($this->className, $resultMapping['name']);
1394
                }
1395
1396 24
                $entityClassName                                = $entityResult['entityClass'];
1397 24
                $resultMapping['entities'][$key]['entityClass'] = $entityClassName;
1398
1399 24
                if (isset($entityResult['fields'])) {
1400 20
                    foreach ($entityResult['fields'] as $k => $field) {
1401 20
                        if (! isset($field['name'])) {
1402
                            throw MappingException::missingResultSetMappingFieldName($this->className, $resultMapping['name']);
1403
                        }
1404
1405 20
                        if (! isset($field['column'])) {
1406 13
                            $fieldName = $field['name'];
1407
1408 13
                            if (strpos($fieldName, '.')) {
1409 6
                                list(, $fieldName) = explode('.', $fieldName);
1410
                            }
1411
1412 24
                            $resultMapping['entities'][$key]['fields'][$k]['column'] = $fieldName;
1413
                        }
1414
                    }
1415
                }
1416
            }
1417
        }
1418
1419 24
        $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping;
1420 24
    }
1421
1422
    /**
1423
     * Registers a custom repository class for the entity class.
1424
     *
1425
     * @param string|null $repositoryClassName The class name of the custom mapper.
1426
     */
1427 31
    public function setCustomRepositoryClassName(?string $repositoryClassName)
1428
    {
1429 31
        $this->customRepositoryClassName = $repositoryClassName;
1430 31
    }
1431
1432 173
    public function getCustomRepositoryClassName() : ?string
1433
    {
1434 173
        return $this->customRepositoryClassName;
1435
    }
1436
1437
    /**
1438
     * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
1439
     *
1440
     * @param string $lifecycleEvent
1441
     *
1442
     * @return bool
1443
     */
1444
    public function hasLifecycleCallbacks($lifecycleEvent)
1445
    {
1446
        return isset($this->lifecycleCallbacks[$lifecycleEvent]);
1447
    }
1448
1449
    /**
1450
     * Gets the registered lifecycle callbacks for an event.
1451
     *
1452
     * @param string $event
1453
     *
1454
     * @return string[]
1455
     */
1456
    public function getLifecycleCallbacks($event)
1457
    {
1458
        return $this->lifecycleCallbacks[$event] ?? [];
1459
    }
1460
1461
    /**
1462
     * Adds a lifecycle callback for entities of this class.
1463
     *
1464
     * @param string $callback
1465
     * @param string $event
1466
     */
1467 17
    public function addLifecycleCallback($callback, $event)
1468
    {
1469 17
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
1470 3
            return;
1471
        }
1472
1473 17
        $this->lifecycleCallbacks[$event][] = $callback;
1474 17
    }
1475
1476
    /**
1477
     * Sets the lifecycle callbacks for entities of this class.
1478
     * Any previously registered callbacks are overwritten.
1479
     *
1480
     * @param string[][] $callbacks
1481
     */
1482 98
    public function setLifecycleCallbacks(array $callbacks) : void
1483
    {
1484 98
        $this->lifecycleCallbacks = $callbacks;
1485 98
    }
1486
1487
    /**
1488
     * Adds a entity listener for entities of this class.
1489
     *
1490
     * @param string $eventName The entity lifecycle event.
1491
     * @param string $class     The listener class.
1492
     * @param string $method    The listener callback method.
1493
     *
1494
     * @throws MappingException
1495
     */
1496 19
    public function addEntityListener(string $eventName, string $class, string $method) : void
1497
    {
1498
        $listener = [
1499 19
            'class'  => $class,
1500 19
            'method' => $method,
1501
        ];
1502
1503 19
        if (! class_exists($class)) {
1504 1
            throw MappingException::entityListenerClassNotFound($class, $this->className);
1505
        }
1506
1507 18
        if (! method_exists($class, $method)) {
1508 1
            throw MappingException::entityListenerMethodNotFound($class, $method, $this->className);
1509
        }
1510
1511 17
        if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
1512 1
            throw MappingException::duplicateEntityListener($class, $method, $this->className);
1513
        }
1514
1515 17
        $this->entityListeners[$eventName][] = $listener;
1516 17
    }
1517
1518
    /**
1519
     * Sets the discriminator column definition.
1520
     *
1521
     * @throws MappingException
1522
     *
1523
     * @see getDiscriminatorColumn()
1524
     */
1525 96
    public function setDiscriminatorColumn(DiscriminatorColumnMetadata $discriminatorColumn) : void
1526
    {
1527 96
        if (isset($this->fieldNames[$discriminatorColumn->getColumnName()])) {
1528 1
            throw MappingException::duplicateColumnName($this->className, $discriminatorColumn->getColumnName());
1529
        }
1530
1531 95
        $discriminatorColumn->setTableName($discriminatorColumn->getTableName() ?? $this->getTableName());
1532
1533 95
        $allowedTypeList = ['boolean', 'array', 'object', 'datetime', 'time', 'date'];
1534
1535 95
        if (in_array($discriminatorColumn->getTypeName(), $allowedTypeList, true)) {
1536
            throw MappingException::invalidDiscriminatorColumnType($discriminatorColumn->getTypeName());
1537
        }
1538
1539 95
        $this->discriminatorColumn = $discriminatorColumn;
1540 95
    }
1541
1542
    /**
1543
     * Sets the discriminator values used by this class.
1544
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
1545
     *
1546
     * @param string[] $map
1547
     *
1548
     * @throws MappingException
1549
     */
1550 93
    public function setDiscriminatorMap(array $map) : void
1551
    {
1552 93
        foreach ($map as $value => $className) {
1553 93
            $this->addDiscriminatorMapClass($value, $className);
1554
        }
1555 93
    }
1556
1557
    /**
1558
     * Adds one entry of the discriminator map with a new class and corresponding name.
1559
     *
1560
     * @param string|int $name
1561
     *
1562
     * @throws MappingException
1563
     */
1564 93
    public function addDiscriminatorMapClass($name, string $className) : void
1565
    {
1566 93
        $this->discriminatorMap[$name] = $className;
1567
1568 93
        if ($this->className === $className) {
1569 79
            $this->discriminatorValue = $name;
1570
1571 79
            return;
1572
        }
1573
1574 92
        if (! (class_exists($className) || interface_exists($className))) {
1575
            throw MappingException::invalidClassInDiscriminatorMap($className, $this->className);
1576
        }
1577
1578 92
        if (is_subclass_of($className, $this->className) && ! in_array($className, $this->subClasses, true)) {
1579 87
            $this->subClasses[] = $className;
1580
        }
1581 92
    }
1582
1583 1026
    public function getValueGenerationPlan() : ValueGenerationPlan
1584
    {
1585 1026
        return $this->valueGenerationPlan;
1586
    }
1587
1588 360
    public function setValueGenerationPlan(ValueGenerationPlan $valueGenerationPlan) : void
1589
    {
1590 360
        $this->valueGenerationPlan = $valueGenerationPlan;
1591 360
    }
1592
1593
    /**
1594
     * Checks whether the class has a named native query with the given query name.
1595
     *
1596
     * @param string $queryName
1597
     */
1598 1
    public function hasNamedNativeQuery($queryName) : bool
1599
    {
1600 1
        return isset($this->namedNativeQueries[$queryName]);
1601
    }
1602
1603
    /**
1604
     * Checks whether the class has a named native query with the given query name.
1605
     *
1606
     * @param string $name
1607
     */
1608 1
    public function hasSqlResultSetMapping($name) : bool
1609
    {
1610 1
        return isset($this->sqlResultSetMappings[$name]);
1611
    }
1612
1613
    /**
1614
     * Marks this class as read only, no change tracking is applied to it.
1615
     */
1616 2
    public function asReadOnly() : void
1617
    {
1618 2
        $this->readOnly = true;
1619 2
    }
1620
1621
    /**
1622
     * Whether this class is read only or not.
1623
     */
1624 444
    public function isReadOnly() : bool
1625
    {
1626 444
        return $this->readOnly;
1627
    }
1628
1629 1086
    public function isVersioned() : bool
1630
    {
1631 1086
        return $this->versionProperty !== null;
1632
    }
1633
1634
    /**
1635
     * Map Embedded Class
1636
     *
1637
     * @param mixed[] $mapping
1638
     *
1639
     * @throws MappingException
1640
     */
1641
    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

1641
    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...
1642
    {
1643
        /*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...
1644
            throw MappingException::duplicateProperty($this->className, $this->getProperty($mapping['fieldName']));
1645
        }
1646
1647
        $this->embeddedClasses[$mapping['fieldName']] = [
1648
            'class'          => $this->fullyQualifiedClassName($mapping['class']),
1649
            'columnPrefix'   => $mapping['columnPrefix'],
1650
            'declaredField'  => $mapping['declaredField'] ?? null,
1651
            'originalField'  => $mapping['originalField'] ?? null,
1652
            'declaringClass' => $this,
1653
        ];*/
1654
    }
1655
1656
    /**
1657
     * Inline the embeddable class
1658
     *
1659
     * @param string $property
1660
     */
1661
    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

1661
    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

1661
    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...
1662
    {
1663
        /*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...
1664
            $fieldMapping['fieldName']     = $property . "." . $fieldName;
1665
            $fieldMapping['originalClass'] = $fieldMapping['originalClass'] ?? $embeddable->getClassName();
1666
            $fieldMapping['originalField'] = $fieldMapping['originalField'] ?? $fieldName;
1667
            $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
1668
                ? $property . '.' . $fieldMapping['declaredField']
1669
                : $property;
1670
1671
            if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
1672
                $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
1673
            } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
1674
                $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName(
1675
                    $property,
1676
                    $fieldMapping['columnName'],
1677
                    $this->reflectionClass->getName(),
1678
                    $embeddable->reflectionClass->getName()
1679
                );
1680
            }
1681
1682
            $this->mapField($fieldMapping);
1683
        }*/
1684
    }
1685
}
1686