Failed Conditions
Pull Request — develop (#6684)
by Michael
61:07
created

ClassMetadata::validateIdentifier()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 0
cp 0
rs 9.2
c 0
b 0
f 0
cc 4
eloc 5
nc 3
nop 0
crap 20
1
<?php
2
3
4
declare(strict_types=1);
5
6
namespace Doctrine\ORM\Mapping;
7
8
use Doctrine\ORM\Cache\CacheException;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\Factory\NamingStrategy;
11
use Doctrine\ORM\Reflection\ReflectionService;
12
use Doctrine\ORM\Sequencing\Planning\ValueGenerationPlan;
13
use Doctrine\ORM\Utility\PersisterHelper;
14
15
/**
16
 * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
17
 * of an entity and its associations.
18
 *
19
 * @author Roman Borschel <[email protected]>
20
 * @author Jonathan H. Wage <[email protected]>
21
 * @author Guilherme Blanco <[email protected]>
22
 * @since 2.0
23
 */
24
class ClassMetadata extends ComponentMetadata implements TableOwner
25
{
26
    /**
27
     * The name of the custom repository class used for the entity class.
28
     * (Optional).
29
     *
30
     * @var string
31
     */
32
    protected $customRepositoryClassName;
33
34
    /**
35
     * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
36
     *
37
     * @var boolean
38
     */
39
    public $isMappedSuperclass = false;
40
41
    /**
42
     * READ-ONLY: Whether this class describes the mapping of an embeddable class.
43
     *
44
     * @var boolean
45
     */
46
    public $isEmbeddedClass = false;
47
48
    /**
49
     * Whether this class describes the mapping of a read-only class.
50
     * That means it is never considered for change-tracking in the UnitOfWork.
51
     * It is a very helpful performance optimization for entities that are immutable,
52
     * either in your domain or through the relation database (coming from a view,
53
     * or a history table for example).
54
     *
55
     * @var boolean
56
     */
57
    private $readOnly = false;
58
59
    /**
60
     * The names of all subclasses (descendants).
61
     *
62
     * @var array
63
     */
64
    protected $subClasses = [];
65
66
    /**
67
     * READ-ONLY: The names of all embedded classes based on properties.
68
     *
69
     * @var array
70
     */
71
    //public $embeddedClasses = [];
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...
72
73
    /**
74
     * The named queries allowed to be called directly from Repository.
75
     *
76
     * @var array
77
     */
78
    protected $namedQueries = [];
79
80
    /**
81
     * READ-ONLY: The named native queries allowed to be called directly from Repository.
82
     *
83
     * A native SQL named query definition has the following structure:
84
     * <pre>
85
     * array(
86
     *     'name'               => <query name>,
87
     *     'query'              => <sql query>,
88
     *     'resultClass'        => <class of the result>,
89
     *     'resultSetMapping'   => <name of a SqlResultSetMapping>
90
     * )
91
     * </pre>
92
     *
93
     * @var array
94
     */
95
    public $namedNativeQueries = [];
96
97
    /**
98
     * READ-ONLY: The mappings of the results of native SQL queries.
99
     *
100
     * A native result mapping definition has the following structure:
101
     * <pre>
102
     * array(
103
     *     'name'               => <result name>,
104
     *     'entities'           => array(<entity result mapping>),
105
     *     'columns'            => array(<column result mapping>)
106
     * )
107
     * </pre>
108
     *
109
     * @var array
110
     */
111
    public $sqlResultSetMappings = [];
112
113
    /**
114
     * READ-ONLY: The registered lifecycle callbacks for entities of this class.
115
     *
116
     * @var array<string, array<string>>
117
     */
118
    public $lifecycleCallbacks = [];
119
120
    /**
121
     * READ-ONLY: The registered entity listeners.
122
     *
123
     * @var array
124
     */
125
    public $entityListeners = [];
126
127
    /**
128
     * READ-ONLY: The field names of all fields that are part of the identifier/primary key
129
     * of the mapped entity class.
130
     *
131
     * @var string[]
132
     */
133
    public $identifier = [];
134
135
    /**
136
     * READ-ONLY: The inheritance mapping type used by the class.
137
     *
138
     * @var string
139
     */
140
    public $inheritanceType = InheritanceType::NONE;
141
142
    /**
143
     * READ-ONLY: The policy used for change-tracking on entities of this class.
144
     *
145
     * @var string
146
     */
147
    public $changeTrackingPolicy = ChangeTrackingPolicy::DEFERRED_IMPLICIT;
148
149
    /**
150
     * READ-ONLY: The discriminator value of this class.
151
     *
152
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
153
     * where a discriminator column is used.</b>
154
     *
155
     * @var mixed
156
     *
157
     * @see discriminatorColumn
158
     */
159
    public $discriminatorValue;
160
161
    /**
162
     * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
163
     *
164
     * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
165
     * where a discriminator column is used.</b>
166
     *
167
     * @var array<string, string>
168
     *
169
     * @see discriminatorColumn
170
     */
171
    public $discriminatorMap = [];
172
173
    /**
174
     * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
175
     * inheritance mappings.
176
     *
177
     * @var DiscriminatorColumnMetadata
178
     */
179
    public $discriminatorColumn;
180
181
    /**
182
     * READ-ONLY: The primary table metadata.
183
     *
184
     * @var TableMetadata
185
     */
186
    public $table;
187
188
    /**
189
     * READ-ONLY: An array of field names. Used to look up field names from column names.
190
     * Keys are column names and values are field names.
191
     *
192
     * @var array<string, string>
193
     */
194
    public $fieldNames = [];
195
196
    /**
197
     * READ-ONLY: The field which is used for versioning in optimistic locking (if any).
198
     *
199
     * @var FieldMetadata|null
200
     */
201
    public $versionProperty;
202
203
    /**
204
     * NamingStrategy determining the default column and table names.
205
     *
206
     * @var NamingStrategy
207
     */
208
    protected $namingStrategy;
209
210
    /**
211
     * Value generation plan is responsible for generating values for auto-generated fields.
212
     *
213
     * @var ValueGenerationPlan
214
     */
215
    protected $valueGenerationPlan;
216
217
    /**
218
     * Initializes a new ClassMetadata instance that will hold the object-relational mapping
219
     * metadata of the class with the given name.
220
     *
221
     * @param string                       $entityName              The name of the entity class.
222
     * @param ClassMetadataBuildingContext $metadataBuildingContext
223
     */
224
    public function __construct(
225
        string $entityName,
226
        ClassMetadataBuildingContext $metadataBuildingContext
227
    )
228
    {
229
        parent::__construct($entityName, $metadataBuildingContext);
230
231
        $this->namingStrategy = $metadataBuildingContext->getNamingStrategy();
232
    }
233
234
    /**
235
     * @todo guilhermeblanco Remove once ClassMetadataFactory is finished
236
     *
237
     * @param string $className
238
     */
239
    public function setClassName(string $className)
240
    {
241
        $this->className = $className;
242
    }
243
244
    /**
245
     * @return \ArrayIterator
246
     */
247
    public function getColumnsIterator() : \ArrayIterator
248
    {
249
        $iterator = parent::getColumnsIterator();
250
251
        if ($this->discriminatorColumn) {
252
            $iterator->offsetSet($this->discriminatorColumn->getColumnName(), $this->discriminatorColumn);
253
        }
254
255
        return $iterator;
256
    }
257
258
    /**
259
     * @return \ArrayIterator
260
     */
261
    public function getAncestorsIterator() : \ArrayIterator
262
    {
263
        $ancestors = new \ArrayIterator();
264
        $parent    = $this;
265
266
        while (($parent = $parent->parent) !== null) {
267
            if ($parent instanceof ClassMetadata && $parent->isMappedSuperclass) {
268
                continue;
269
            }
270
271
            $ancestors->append($parent);
272
        }
273
274
        return $ancestors;
275
    }
276
277
    /**
278
     * @return string
279
     */
280
    public function getRootClassName() : string
281
    {
282
        return ($this->parent instanceof ClassMetadata && ! $this->parent->isMappedSuperclass)
283
            ? $this->parent->getRootClassName()
284
            : $this->className
285
        ;
286
    }
287
288
    /**
289
     * Handles metadata cloning nicely.
290
     */
291
    public function __clone()
292
    {
293
        if ($this->cache) {
294
            $this->cache = clone $this->cache;
295
        }
296
297
        foreach ($this->declaredProperties as $name => $property) {
298
            $this->declaredProperties[$name] = clone $property;
299
        }
300
    }
301
302
    /**
303
     * Creates a string representation of this instance.
304
     *
305
     * @return string The string representation of this instance.
306
     *
307
     * @todo Construct meaningful string representation.
308
     */
309
    public function __toString()
310
    {
311
        return __CLASS__ . '@' . spl_object_hash($this);
312
    }
313
314
    /**
315
     * Determines which fields get serialized.
316
     *
317
     * It is only serialized what is necessary for best unserialization performance.
318
     * That means any metadata properties that are not set or empty or simply have
319
     * their default value are NOT serialized.
320
     *
321
     * Parts that are also NOT serialized because they can not be properly unserialized:
322
     * - reflectionClass
323
     *
324
     * @return array The names of all the fields that should be serialized.
325
     */
326
    public function __sleep()
327
    {
328
        $serialized = [];
329
330
        // This metadata is always serialized/cached.
331
        $serialized = array_merge($serialized, [
332
            'declaredProperties',
333
            'fieldNames',
334
            //'embeddedClasses',
335
            'identifier',
336
            'className',
337
            'parent',
338
            'table',
339
            'valueGenerationPlan',
340
        ]);
341
342
        // The rest of the metadata is only serialized if necessary.
343
        if ($this->changeTrackingPolicy !== ChangeTrackingPolicy::DEFERRED_IMPLICIT) {
344
            $serialized[] = 'changeTrackingPolicy';
345
        }
346
347
        if ($this->customRepositoryClassName) {
348
            $serialized[] = 'customRepositoryClassName';
349
        }
350
351
        if ($this->inheritanceType !== InheritanceType::NONE) {
352
            $serialized[] = 'inheritanceType';
353
            $serialized[] = 'discriminatorColumn';
354
            $serialized[] = 'discriminatorValue';
355
            $serialized[] = 'discriminatorMap';
356
            $serialized[] = 'subClasses';
357
        }
358
359
        if ($this->isMappedSuperclass) {
360
            $serialized[] = 'isMappedSuperclass';
361
        }
362
363
        if ($this->isEmbeddedClass) {
364
            $serialized[] = 'isEmbeddedClass';
365
        }
366
367
        if ($this->isVersioned()) {
368
            $serialized[] = 'versionProperty';
369
        }
370
371
        if ($this->lifecycleCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lifecycleCallbacks of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
372
            $serialized[] = 'lifecycleCallbacks';
373
        }
374
375
        if ($this->entityListeners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityListeners of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
376
            $serialized[] = 'entityListeners';
377
        }
378
379
        if ($this->namedQueries) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namedQueries of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
380
            $serialized[] = 'namedQueries';
381
        }
382
383
        if ($this->namedNativeQueries) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namedNativeQueries of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
384
            $serialized[] = 'namedNativeQueries';
385
        }
386
387
        if ($this->sqlResultSetMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sqlResultSetMappings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
388
            $serialized[] = 'sqlResultSetMappings';
389
        }
390
391
        if ($this->cache) {
392
            $serialized[] = 'cache';
393
        }
394
395
        if ($this->readOnly) {
396
            $serialized[] = 'readOnly';
397
        }
398
399
        return $serialized;
400
    }
401
402
    /**
403
     * Restores some state that can not be serialized/unserialized.
404
     *
405
     * @param ReflectionService $reflectionService
406
     *
407
     * @return void
408
     */
409
    public function wakeupReflection(ReflectionService $reflectionService) : void
410
    {
411
        // Restore ReflectionClass and properties
412
        $this->reflectionClass = $reflectionService->getClass($this->className);
413
414
        if (! $this->reflectionClass) {
415
            return;
416
        }
417
418
        $this->className = $this->reflectionClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $this->reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
419
420
        foreach ($this->declaredProperties as $property) {
421
            /** @var Property $property */
422
            $property->wakeupReflection($reflectionService);
423
        }
424
    }
425
426
    /**
427
     * Validates Identifier.
428
     *
429
     * @return void
430
     *
431
     * @throws MappingException
432
     */
433
    public function validateIdentifier() : void
434
    {
435
        if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
436
            return;
437
        }
438
439
        // Verify & complete identifier mapping
440
        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...
441
            throw MappingException::identifierRequired($this->className);
442
        }
443
    }
444
445
    /**
446
     * Validates field value generators.
447
     */
448
    public function validateValueGenerators() : void
449
    {
450
        foreach ($this->getPropertiesIterator() as $property) {
451
            if (! $property instanceof FieldMetadata || ! $property->hasValueGenerator()) {
452
                continue;
453
            }
454
455
            $generator = $property->getValueGenerator();
456
457
            if ($generator->getType() !== GeneratorType::IDENTITY) {
458
                continue;
459
            }
460
461
            if (! $property->isPrimaryKey()) {
462
                throw MappingException::nonPrimaryidentityGeneratorNotSupported($this->className);
463
            }
464
465
            if ($this->isIdentifierComposite()) {
466
                throw MappingException::compositeIdentityGeneratorNotSupported($this->className);
467
            }
468
        }
469
    }
470
471
    /**
472
     * Validates association targets actually exist.
473
     *
474
     * @return void
475
     *
476
     * @throws MappingException
477
     */
478
    public function validateAssociations() : void
479
    {
480
        array_map(
481
            function (Property $property) {
482
                if (! ($property instanceof AssociationMetadata)) {
483
                    return;
484
                }
485
486
                $targetEntity = $property->getTargetEntity();
487
488
                if (! class_exists($targetEntity)) {
489
                    throw MappingException::invalidTargetEntityClass($targetEntity, $this->className, $property->getName());
490
                }
491
            },
492
            $this->declaredProperties
493
        );
494
    }
495
496
    /**
497
     * Validates lifecycle callbacks.
498
     *
499
     * @param ReflectionService $reflectionService
500
     *
501
     * @return void
502
     *
503
     * @throws MappingException
504
     */
505
    public function validateLifecycleCallbacks(ReflectionService $reflectionService) : void
506
    {
507
        foreach ($this->lifecycleCallbacks as $callbacks) {
508
            /** @var array $callbacks */
509
            foreach ($callbacks as $callbackFuncName) {
510
                if (! $reflectionService->hasPublicMethod($this->className, $callbackFuncName)) {
511
                    throw MappingException::lifecycleCallbackMethodNotFound($this->className, $callbackFuncName);
512
                }
513
            }
514
        }
515
    }
516
517
    /**
518
     * Sets the change tracking policy used by this class.
519
     *
520
     * @param string $policy
521
     *
522
     * @return void
523
     */
524
    public function setChangeTrackingPolicy(string $policy) : void
525
    {
526
        $this->changeTrackingPolicy = $policy;
527
    }
528
529
    /**
530
     * Checks whether a field is part of the identifier/primary key field(s).
531
     *
532
     * @param string $fieldName The field name.
533
     *
534
     * @return bool TRUE if the field is part of the table identifier/primary key field(s), FALSE otherwise.
535
     */
536
    public function isIdentifier(string $fieldName) : bool
537
    {
538
        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...
539
            return false;
540
        }
541
542
        if (! $this->isIdentifierComposite()) {
543
            return $fieldName === $this->identifier[0];
544
        }
545
546
        return in_array($fieldName, $this->identifier, true);
547
    }
548
549
    /**
550
     * @return bool
551
     */
552
    public function isIdentifierComposite() : bool
553
    {
554
        return isset($this->identifier[1]);
555
    }
556
557
    /**
558
     * Gets the named query.
559
     *
560
     * @see ClassMetadata::$namedQueries
561
     *
562
     * @param string $queryName The query name.
563
     *
564
     * @return string
565
     *
566
     * @throws MappingException
567
     */
568
    public function getNamedQuery($queryName) : string
569
    {
570
        if (! isset($this->namedQueries[$queryName])) {
571
            throw MappingException::queryNotFound($this->className, $queryName);
572
        }
573
574
        return $this->namedQueries[$queryName];
575
    }
576
577
    /**
578
     * Gets all named queries of the class.
579
     *
580
     * @return array
581
     */
582
    public function getNamedQueries() : array
583
    {
584
        return $this->namedQueries;
585
    }
586
587 578
    /**
588
     * Gets the named native query.
589 578
     *
590 578
     * @see ClassMetadata::$namedNativeQueries
591 578
     *
592 578
     * @param string $queryName The query name.
593 578
     *
594
     * @return array
595
     *
596
     * @throws MappingException
597
     */
598
    public function getNamedNativeQuery($queryName) : array
599
    {
600 224
        if ( ! isset($this->namedNativeQueries[$queryName])) {
601
            throw MappingException::queryNotFound($this->className, $queryName);
602 224
        }
603
604
        return $this->namedNativeQueries[$queryName];
605
    }
606
607
    /**
608
     * Gets all named native queries of the class.
609
     *
610
     * @return array
611
     */
612 1
    public function getNamedNativeQueries() : array
613
    {
614 1
        return $this->namedNativeQueries;
615
    }
616
617
    /**
618
     * Gets the result set mapping.
619
     *
620
     * @see ClassMetadata::$sqlResultSetMappings
621
     *
622
     * @param string $name The result set mapping name.
623
     *
624
     * @return array
625
     *
626
     * @throws MappingException
627
     */
628
    public function getSqlResultSetMapping($name)
629
    {
630
        if (! isset($this->sqlResultSetMappings[$name])) {
631
            throw MappingException::resultMappingNotFound($this->className, $name);
632
        }
633
634
        return $this->sqlResultSetMappings[$name];
635
    }
636
637
    /**
638
     * Gets all sql result set mappings of the class.
639
     *
640
     * @return array
641
     */
642
    public function getSqlResultSetMappings()
643 466
    {
644
        return $this->sqlResultSetMappings;
645 466
    }
646 91
647
    /**
648 91
     * Validates & completes the basic mapping information for field mapping.
649 91
     *
650
     * @param FieldMetadata $property
651 91
     *
652 91
     * @throws MappingException If something is wrong with the mapping.
653
     */
654
    protected function validateAndCompleteFieldMapping(FieldMetadata $property)
655
    {
656 91
        $fieldName  = $property->getName();
657
        $columnName = $property->getColumnName();
658
659 446
        if (empty($columnName)) {
660 446
            $columnName = $this->namingStrategy->propertyToColumnName($fieldName, $this->className);
661
662 446
            $property->setColumnName($columnName);
663 27
        }
664
665
        if (! $this->isMappedSuperclass) {
666 423
            $property->setTableName($this->getTableName());
667
        }
668
669
        // Check for already declared column
670
        if (isset($this->fieldNames[$columnName]) ||
671
            ($this->discriminatorColumn !== null && $this->discriminatorColumn->getColumnName() === $columnName)) {
672
            throw MappingException::duplicateColumnName($this->className, $columnName);
673
        }
674
675
        // Complete id mapping
676
        if ($property->isPrimaryKey()) {
677 6
            if ($this->versionProperty !== null && $this->versionProperty->getName() === $fieldName) {
678
                throw MappingException::cannotVersionIdField($this->className, $fieldName);
679 6
            }
680 6
681
            if ($property->getType()->canRequireSQLConversion()) {
682 6
                throw MappingException::sqlConversionNotAllowedForPrimaryKeyProperties($property);
0 ignored issues
show
Bug introduced by
The call to sqlConversionNotAllowedForPrimaryKeyProperties() misses a required argument $property.

This check looks for function calls that miss required arguments.

Loading history...
Documentation introduced by
$property is of type object<Doctrine\ORM\Mapping\FieldMetadata>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
683
            };
684
685
            if (! in_array($fieldName, $this->identifier)) {
686
                $this->identifier[] = $fieldName;
687
            }
688
        }
689
690
        $this->fieldNames[$columnName] = $fieldName;
691
    }
692
693 24
    /**
694
     * Validates & completes the basic mapping information for field mapping.
695 24
     *
696 24
     * @param VersionFieldMetadata $property
697
     *
698
     * @throws MappingException If something is wrong with the mapping.
699
     */
700
    protected function validateAndCompleteVersionFieldMapping(VersionFieldMetadata $property)
701
    {
702
        $this->versionProperty = $property;
703
704
        $options = $property->getOptions();
705
706 312
        if (isset($options['default'])) {
707
            return;
708 312
        }
709
710
        if (in_array($property->getTypeName(), ['integer', 'bigint', 'smallint'])) {
711
            $property->setOptions(array_merge($options, ['default' => 1]));
712
713
            return;
714
        }
715
716
        if ($property->getTypeName() === 'datetime') {
717
            $property->setOptions(array_merge($options, ['default' => 'CURRENT_TIMESTAMP']));
718
719
            return;
720
        }
721
722
        throw MappingException::unsupportedOptimisticLockingType($property->getType());
0 ignored issues
show
Documentation introduced by
$property->getType() is of type object<Doctrine\DBAL\Types\Type>|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
723
    }
724
725
    /**
726
     * Validates & completes the basic mapping information that is common to all
727
     * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
728
     *
729
     * @param AssociationMetadata $property
730
     *
731
     * @throws MappingException If something is wrong with the mapping.
732
     * @throws CacheException   If entity is not cacheable.
733
     */
734
    protected function validateAndCompleteAssociationMapping(AssociationMetadata $property)
735
    {
736 7
        $fieldName    = $property->getName();
737
        $targetEntity = $property->getTargetEntity();
738
739
        if (! $targetEntity) {
740 7
            throw MappingException::missingTargetEntity($fieldName);
741
        }
742
743
        $property->setSourceEntity($this->className);
744
        $property->setOwningSide($property->getMappedBy() === null);
745
        $property->setTargetEntity($targetEntity);
746
747
        // Complete id mapping
748
        if ($property->isPrimaryKey()) {
749
            if ($property->isOrphanRemoval()) {
750
                throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->className, $fieldName);
751
            }
752
753 7
            if ( ! in_array($property->getName(), $this->identifier)) {
754
                if ($property instanceof ToOneAssociationMetadata && count($property->getJoinColumns()) >= 2) {
755
                    throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
756
                        $property->getTargetEntity(),
757 7
                        $this->className,
758 1
                        $fieldName
759
                    );
760
                }
761 7
762 1
                $this->identifier[] = $property->getName();
763 1
            }
764 1
765 1
            if ($this->cache && !$property->getCache()) {
766 1
                throw CacheException::nonCacheableEntityAssociation($this->className, $fieldName);
767 1
            }
768
769
            if ($property instanceof ToManyAssociationMetadata) {
770 7
                throw MappingException::illegalToManyIdentifierAssociation($this->className, $property->getName());
771 1
            }
772 1
        }
773
774
        // Cascades
775
        $cascadeTypes = ['remove', 'persist', 'refresh'];
776
        $cascades     = array_map('strtolower', $property->getCascade());
777 7
778 1
        if (in_array('all', $cascades)) {
779
            $cascades = $cascadeTypes;
780
        }
781 7
782 1
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
783
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
784
785 7
            throw MappingException::invalidCascadeOption($diffCascades, $this->className, $fieldName);
786
        }
787
788
        $property->setCascade($cascades);
789 7
    }
790
791
    /**
792
     * Validates & completes a to-one association mapping.
793 7
     *
794
     * @param ToOneAssociationMetadata $property The association mapping to validate & complete.
795
     *
796
     * @throws \RuntimeException
797 7
     * @throws MappingException
798 1
     */
799
    protected function validateAndCompleteToOneAssociationMetadata(ToOneAssociationMetadata $property)
800
    {
801 7
        $fieldName = $property->getName();
802 1
803
        if ($property->getJoinColumns()) {
804
            $property->setOwningSide(true);
805 7
        }
806
807
        if ($property->isOwningSide()) {
808
            if (empty($property->getJoinColumns())) {
809 7
                // Apply default join column
810
                $property->addJoinColumn(new JoinColumnMetadata());
811
            }
812
813 7
            $uniqueConstraintColumns = [];
814 1
815
            foreach ($property->getJoinColumns() as $joinColumn) {
816
                /** @var JoinColumnMetadata $joinColumn */
817 7
                if ($property instanceof OneToOneAssociationMetadata && $this->inheritanceType !== InheritanceType::SINGLE_TABLE) {
818
                    if (1 === count($property->getJoinColumns())) {
819
                        if (! $property->isPrimaryKey()) {
820
                            $joinColumn->setUnique(true);
821 7
                        }
822
                    } else {
823
                        $uniqueConstraintColumns[] = $joinColumn->getColumnName();
824
                    }
825 7
                }
826
827
                $joinColumn->setTableName(! $this->isMappedSuperclass ? $this->getTableName() : null);
828
829
                if (! $joinColumn->getColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $joinColumn->getColumnName() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
830
                    $joinColumn->setColumnName($this->namingStrategy->joinColumnName($fieldName, $this->className));
831
                }
832
833 671
                if (! $joinColumn->getReferencedColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $joinColumn->getReferencedColumnName() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
834
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
835 671
                }
836
837
                $this->fieldNames[$joinColumn->getColumnName()] = $fieldName;
838
            }
839
840
            if ($uniqueConstraintColumns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uniqueConstraintColumns 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...
841
                if ( ! $this->table) {
842
                    throw new \RuntimeException(
843
                        "ClassMetadata::setTable() has to be called before defining a one to one relationship."
844
                    );
845 1988
                }
846
847
                $this->table->addUniqueConstraint(
848 1988
                    [
849 1988
                        'name'    => sprintf('%s_uniq', $fieldName),
850
                        'columns' => $uniqueConstraintColumns,
851 1988
                        'options' => [],
852
                        'flags'   => [],
853
                    ]
854
                );
855
            }
856
        }
857
858
        if ($property->isOrphanRemoval()) {
859
            $cascades = $property->getCascade();
860
861
            if (! in_array('remove', $cascades)) {
862
                $cascades[] = 'remove';
863
864
                $property->setCascade($cascades);
865
            }
866
867
            // @todo guilhermeblanco where is this used?
868
            // @todo guilhermeblanco Shouldn￿'t we iterate through JoinColumns to set non-uniqueness?
869
            //$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...
870
        }
871 1988
872
        if ($property->isPrimaryKey() && ! $property->isOwningSide()) {
873
            throw MappingException::illegalInverseIdentifierAssociation($this->className, $fieldName);
874
        }
875
    }
876
877
    /**
878
     * Validates & completes a to-many association mapping.
879
     *
880
     * @param ToManyAssociationMetadata $property The association mapping to validate & complete.
881 1984
     *
882
     * @throws MappingException
883 1984
     */
884
    protected function validateAndCompleteToManyAssociationMetadata(ToManyAssociationMetadata $property)
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed.

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

Loading history...
885
    {
886 1988
        // Do nothing
887 1679
    }
888
889 1988
    /**
890
     * Validates & completes a one-to-one association mapping.
891
     *
892
     * @param OneToOneAssociationMetadata $property The association mapping to validate & complete.
893
     */
894
    protected function validateAndCompleteOneToOneMapping(OneToOneAssociationMetadata $property)
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed.

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

Loading history...
895
    {
896
        // Do nothing
897
    }
898
899 543
    /**
900
     * Validates & completes a many-to-one association mapping.
901 543
     *
902
     * @param ManyToOneAssociationMetadata $property The association mapping to validate & complete.
903 543
     *
904 539
     * @throws MappingException
905
     */
906
    protected function validateAndCompleteManyToOneMapping(ManyToOneAssociationMetadata $property)
907 543
    {
908 543
        // A many-to-one mapping is essentially a one-one backreference
909
        if ($property->isOrphanRemoval()) {
910 543
            throw MappingException::illegalOrphanRemoval($this->className, $property->getName());
911
        }
912
    }
913
914
    /**
915
     * Validates & completes a one-to-many association mapping.
916
     *
917
     * @param OneToManyAssociationMetadata $property The association mapping to validate & complete.
918
     *
919 385
     * @throws MappingException
920
     */
921 385
    protected function validateAndCompleteOneToManyMapping(OneToManyAssociationMetadata $property)
922 44
    {
923
        // OneToMany MUST be inverse side
924
        $property->setOwningSide(false);
925
926 381
        // OneToMany MUST have mappedBy
927 8
        if (! $property->getMappedBy()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $property->getMappedBy() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
928
            throw MappingException::oneToManyRequiresMappedBy($property->getName());
929
        }
930 373
931
        if ($property->isOrphanRemoval()) {
932
            $cascades = $property->getCascade();
933 373
934
            if (! in_array('remove', $cascades)) {
935
                $cascades[] = 'remove';
936
937
                $property->setCascade($cascades);
938
            }
939
        }
940
    }
941
942 382
    /**
943
     * Validates & completes a many-to-many association mapping.
944 382
     *
945 254
     * @param ManyToManyAssociationMetadata $property The association mapping to validate & complete.
946 254
     *
947
     * @throws MappingException
948
     */
949 381
    protected function validateAndCompleteManyToManyMapping(ManyToManyAssociationMetadata $property)
950
    {
951
        if ($property->isOwningSide()) {
952
            // owning side MUST have a join table
953
            $joinTable = $property->getJoinTable() ?: new JoinTableMetadata();
954
955
            $property->setJoinTable($joinTable);
956
957
            if (! $joinTable->getName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $joinTable->getName() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
958
                $joinTableName = $this->namingStrategy->joinTableName(
959
                    $property->getSourceEntity(),
960 382
                    $property->getTargetEntity(),
961
                    $property->getName()
962 382
                );
963 13
964 13
                $joinTable->setName($joinTableName);
965 13
            }
966
967
            $selfReferencingEntityWithoutJoinColumns = $property->getSourceEntity() == $property->getTargetEntity() && ! $joinTable->hasColumns();
968
969 381
            if (! $joinTable->getJoinColumns()) {
970
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
971
                $sourceReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'source' : $referencedColumnName;
972
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getSourceEntity(), $sourceReferenceName);
973
                $joinColumn           = new JoinColumnMetadata();
974 538
975
                $joinColumn->setColumnName($columnName);
976 538
                $joinColumn->setReferencedColumnName($referencedColumnName);
977
                $joinColumn->setOnDelete('CASCADE');
978
979
                $joinTable->addJoinColumn($joinColumn);
980
            }
981
982
            if (! $joinTable->getInverseJoinColumns()) {
983
                $referencedColumnName = $this->namingStrategy->referenceColumnName();
984 19
                $targetReferenceName  = $selfReferencingEntityWithoutJoinColumns ? 'target' : $referencedColumnName;
985
                $columnName           = $this->namingStrategy->joinKeyColumnName($property->getTargetEntity(), $targetReferenceName);
986 19
                $joinColumn           = new JoinColumnMetadata();
987
988
                $joinColumn->setColumnName($columnName);
989
                $joinColumn->setReferencedColumnName($referencedColumnName);
990 19
                $joinColumn->setOnDelete('CASCADE');
991 5
992
                $joinTable->addInverseJoinColumn($joinColumn);
993
            }
994 19
995 19
            foreach ($joinTable->getJoinColumns() as $joinColumn) {
996
                /** @var JoinColumnMetadata $joinColumn */
997
                if (! $joinColumn->getReferencedColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $joinColumn->getReferencedColumnName() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
998
                    $joinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
999
                }
1000
1001
                $referencedColumnName = $joinColumn->getReferencedColumnName();
1002
1003 2
                if (! $joinColumn->getColumnName()) {
1004
                    $columnName = $this->namingStrategy->joinKeyColumnName(
1005 2
                        $property->getSourceEntity(),
1006 2
                        $referencedColumnName
1007
                    );
1008
1009
                    $joinColumn->setColumnName($columnName);
1010
                }
1011
            }
1012
1013
            foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) {
1014 15
                /** @var JoinColumnMetadata $inverseJoinColumn */
1015
                if (! $inverseJoinColumn->getReferencedColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inverseJoinColumn->getReferencedColumnName() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1016 15
                    $inverseJoinColumn->setReferencedColumnName($this->namingStrategy->referenceColumnName());
1017 1
                }
1018 1
1019
                $referencedColumnName = $inverseJoinColumn->getReferencedColumnName();
1020
1021
                if (! $inverseJoinColumn->getColumnName()) {
1022 15
                    $columnName = $this->namingStrategy->joinKeyColumnName(
1023 15
                        $property->getTargetEntity(),
1024
                        $referencedColumnName
1025
                    );
1026 15
1027
                    $inverseJoinColumn->setColumnName($columnName);
1028
                }
1029
            }
1030
        }
1031
    }
1032
1033
    /**
1034
     * {@inheritDoc}
1035
     */
1036 123
    public function getIdentifierFieldNames()
1037
    {
1038 123
        return $this->identifier;
1039 123
    }
1040
1041
    /**
1042
     * Gets the name of the single id field. Note that this only works on
1043
     * entity classes that have a single-field pk.
1044
     *
1045
     * @return string
1046 267
     *
1047
     * @throws MappingException If the class has a composite primary key.
1048 267
     */
1049
    public function getSingleIdentifierFieldName()
1050
    {
1051
        if ($this->isIdentifierComposite()) {
1052
            throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->className);
1053
        }
1054
1055
        if ( ! isset($this->identifier[0])) {
1056 459
            throw MappingException::noIdDefined($this->className);
1057
        }
1058 459
1059
        return $this->identifier[0];
1060
    }
1061
1062
    /**
1063
     * INTERNAL:
1064
     * Sets the mapped identifier/primary key fields of this class.
1065
     * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
1066 290
     *
1067
     * @param array $identifier
1068 290
     *
1069
     * @return void
1070
     */
1071
    public function setIdentifier(array $identifier)
1072
    {
1073
        $this->identifier = $identifier;
1074
    }
1075
1076
    /**
1077
     * {@inheritDoc}
1078
     */
1079 1032
    public function getIdentifier()
1080
    {
1081 1032
        return $this->identifier;
1082 1
    }
1083
1084
    /**
1085 1031
     * {@inheritDoc}
1086 1026
     */
1087
    public function hasField($fieldName)
1088
    {
1089 91
        return isset($this->declaredProperties[$fieldName])
1090
            && $this->declaredProperties[$fieldName] instanceof FieldMetadata;
1091
    }
1092
1093
    /**
1094
     * Returns an array with identifier column names and their corresponding ColumnMetadata.
1095
     *
1096
     * @param EntityManagerInterface $em
1097
     *
1098
     * @return array
1099
     */
1100
    public function getIdentifierColumns(EntityManagerInterface $em) : array
1101
    {
1102
        $columns = [];
1103 8
1104
        foreach ($this->identifier as $idProperty) {
1105 8
            $property = $this->getProperty($idProperty);
1106 8
1107 8
            if ($property instanceof FieldMetadata) {
1108
                $columns[$property->getColumnName()] = $property;
1109
1110
                continue;
1111
            }
1112
1113
            /** @var AssociationMetadata $property */
1114
1115
            // Association defined as Id field
1116
            $targetClass = $em->getClassMetadata($property->getTargetEntity());
1117
1118
            if (! $property->isOwningSide()) {
1119
                $property    = $targetClass->getProperty($property->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1120
                $targetClass = $em->getClassMetadata($property->getTargetEntity());
1121
            }
1122 482
1123
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
1124 482
                ? $property->getJoinTable()->getInverseJoinColumns()
1125
                : $property->getJoinColumns()
1126
            ;
1127
1128 482
            foreach ($joinColumns as $joinColumn) {
1129
                /** @var JoinColumnMetadata $joinColumn */
1130
                $columnName           = $joinColumn->getColumnName();
1131
                $referencedColumnName = $joinColumn->getReferencedColumnName();
1132
1133
                if (! $joinColumn->getType()) {
1134
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $em));
0 ignored issues
show
Documentation introduced by
$targetClass is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1135
                }
1136
1137
                $columns[$columnName] = $joinColumn;
1138
            }
1139
        }
1140
1141
        return $columns;
1142
    }
1143
1144
    /**
1145
     * Gets the name of the primary table.
1146
     *
1147
     * @return string|null
1148
     */
1149 237
    public function getTableName() : ?string
1150
    {
1151 237
        return $this->table->getName();
1152 237
    }
1153 237
1154
    /**
1155
     * Gets primary table's schema name.
1156
     *
1157
     * @return string|null
1158
     */
1159
    public function getSchemaName() : ?string
1160
    {
1161
        return $this->table->getSchema();
1162
    }
1163
1164
    /**
1165
     * Gets the table name to use for temporary identifier tables of this class.
1166
     *
1167 4
     * @return string
1168
     */
1169 4
    public function getTemporaryIdTableName() : string
1170 1
    {
1171
        $schema = null === $this->getSchemaName()
1172
            ? ''
1173 3
            : $this->getSchemaName() . '_'
1174
        ;
1175
1176
        // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
1177
        return $schema . $this->getTableName() . '_id_tmp';
1178
    }
1179
1180
    /**
1181 5
     * Sets the mapped subclasses of this class.
1182
     *
1183 5
     * @todo guilhermeblanco Only used for ClassMetadataTest. Remove if possible!
1184
     *
1185
     * @param array $subclasses The names of all mapped subclasses.
1186
     *
1187
     * @return void
1188
     */
1189
    public function setSubclasses(array $subclasses) : void
1190
    {
1191
        foreach ($subclasses as $subclass) {
1192
            $this->subClasses[] = $subclass;
1193
        }
1194
    }
1195
1196
    /**
1197 15
     * @return array
1198
     */
1199 15
    public function getSubClasses() : array
1200
    {
1201
        return $this->subClasses;
1202
    }
1203 15
1204
    /**
1205
     * Sets the inheritance type used by the class and its subclasses.
1206
     *
1207
     * @param integer $type
1208
     *
1209
     * @return void
1210
     *
1211 2
     * @throws MappingException
1212
     */
1213 2
    public function setInheritanceType($type) : void
1214
    {
1215
        if ( ! $this->isInheritanceType($type)) {
1216
            throw MappingException::invalidInheritanceType($this->className, $type);
1217
        }
1218
1219
        $this->inheritanceType = $type;
0 ignored issues
show
Documentation Bug introduced by
The property $inheritanceType was declared of type string, but $type is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1220
    }
1221
1222
    /**
1223
     * Sets the override property mapping for an entity relationship.
1224
     *
1225
     * @param Property $property
1226
     *
1227 17
     * @return void
1228
     *
1229 17
     * @throws \RuntimeException
1230
     * @throws MappingException
1231
     * @throws CacheException
1232
     */
1233 17
    public function setPropertyOverride(Property $property) : void
1234
    {
1235
        $fieldName = $property->getName();
1236
1237
        if (! isset($this->declaredProperties[$fieldName])) {
1238
            throw MappingException::invalidOverrideFieldName($this->className, $fieldName);
1239
        }
1240
1241 6
        $originalProperty          = $this->getProperty($fieldName);
1242
        $originalPropertyClassName = get_class($originalProperty);
1243 6
1244
        // If moving from transient to persistent, assume it's a new property
1245
        if ($originalPropertyClassName === TransientMetadata::class) {
1246
            unset($this->declaredProperties[$fieldName]);
1247
1248
            $this->addProperty($property);
1249
1250
            return;
1251
        }
1252
1253
        // Do not allow to change property type
1254
        if ($originalPropertyClassName !== get_class($property)) {
1255
            throw MappingException::invalidOverridePropertyType($this->className, $fieldName);
1256 317
        }
1257
1258 317
        // Do not allow to change version property
1259
        if ($originalProperty instanceof VersionFieldMetadata) {
1260 317
            throw MappingException::invalidOverrideVersionField($this->className, $fieldName);
1261 304
        }
1262
1263
        unset($this->declaredProperties[$fieldName]);
1264 317
1265 296
        if ($property instanceof FieldMetadata) {
1266
            // Unset defined fieldName prior to override
1267
            unset($this->fieldNames[$originalProperty->getColumnName()]);
1268 317
1269
            // Revert what should not be allowed to change
1270
            $property->setDeclaringClass($originalProperty->getDeclaringClass());
1271 317
            $property->setPrimaryKey($originalProperty->isPrimaryKey());
1272 314
        } else if ($property instanceof AssociationMetadata) {
1273
            // Unset all defined fieldNames prior to override
1274
            if ($originalProperty instanceof ToOneAssociationMetadata && $originalProperty->isOwningSide()) {
1275
                foreach ($originalProperty->getJoinColumns() as $joinColumn) {
1276
                    unset($this->fieldNames[$joinColumn->getColumnName()]);
1277 317
                }
1278
            }
1279 317
1280 317
            // Override what it should be allowed to change
1281 317
            if ($property->getInversedBy()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $property->getInversedBy() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1282
                $originalProperty->setInversedBy($property->getInversedBy());
1283
            }
1284 317
1285 1
            if ($property->getFetchMode() !== $originalProperty->getFetchMode()) {
1286
                $originalProperty->setFetchMode($property->getFetchMode());
1287
            }
1288
1289 316
            if ($originalProperty instanceof ToOneAssociationMetadata && $property->getJoinColumns()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\Mapping\AssociationMetadata as the method getJoinColumns() does only exist in the following sub-classes of Doctrine\ORM\Mapping\AssociationMetadata: Doctrine\ORM\Mapping\ManyToOneAssociationMetadata, Doctrine\ORM\Mapping\OneToOneAssociationMetadata, Doctrine\ORM\Mapping\ToOneAssociationMetadata. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1290 48
                $originalProperty->setJoinColumns($property->getJoinColumns());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\Mapping\AssociationMetadata as the method getJoinColumns() does only exist in the following sub-classes of Doctrine\ORM\Mapping\AssociationMetadata: Doctrine\ORM\Mapping\ManyToOneAssociationMetadata, Doctrine\ORM\Mapping\OneToOneAssociationMetadata, Doctrine\ORM\Mapping\ToOneAssociationMetadata. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1291 1
            } else if ($originalProperty instanceof ManyToManyAssociationMetadata && $property->getJoinTable()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\Mapping\AssociationMetadata as the method getJoinTable() does only exist in the following sub-classes of Doctrine\ORM\Mapping\AssociationMetadata: Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1292
                $originalProperty->setJoinTable($property->getJoinTable());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\Mapping\AssociationMetadata as the method getJoinTable() does only exist in the following sub-classes of Doctrine\ORM\Mapping\AssociationMetadata: Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1293
            }
1294 47
1295 47
            $property = $originalProperty;
1296
        }
1297
1298
        $this->addProperty($property);
0 ignored issues
show
Bug introduced by
It seems like $property defined by $originalProperty on line 1295 can be null; however, Doctrine\ORM\Mapping\ClassMetadata::addProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1299
    }
1300
1301 47
    /**
1302 47
     * Checks if this entity is the root in any entity-inheritance-hierarchy.
1303
     *
1304
     * @return bool
1305
     */
1306 47
    public function isRootEntity()
1307 24
    {
1308
        return $this->className === $this->getRootClassName();
1309
    }
1310 47
1311 3
    /**
1312
     * Checks whether a mapped field is inherited from a superclass.
1313
     *
1314
     * @param string $fieldName
1315
     *
1316
     * @return boolean TRUE if the field is inherited, FALSE otherwise.
1317 312
     */
1318
    public function isInheritedProperty($fieldName)
1319
    {
1320
        $declaringClass = $this->declaredProperties[$fieldName]->getDeclaringClass();
1321 312
1322
        return ! ($declaringClass->className === $this->className);
1323
    }
1324
1325
    /**
1326 312
     * {@inheritdoc}
1327 171
     */
1328
    public function setTable(TableMetadata $table) : void
1329
    {
1330 312
        $this->table = $table;
1331 3
1332
        if (empty($table->getName())) {
1333
            $table->setName($this->namingStrategy->classToTableName($this->className));
1334
        }
1335 309
    }
1336 64
1337
    /**
1338
     * Checks whether the given type identifies an inheritance type.
1339
     *
1340 309
     * @param integer $type
1341 309
     *
1342
     * @return boolean TRUE if the given type identifies an inheritance type, FALSe otherwise.
1343 309
     */
1344 30
    private function isInheritanceType($type)
1345
    {
1346
        return $type == InheritanceType::NONE
1347 309
            || $type == InheritanceType::SINGLE_TABLE
1348 1
            || $type == InheritanceType::JOINED
1349
            || $type == InheritanceType::TABLE_PER_CLASS;
1350 1
    }
1351 1
1352
    /**
1353
     * @param string $columnName
1354
     *
1355 308
     * @return LocalColumnMetadata|null
1356
     */
1357 308
    public function getColumn(string $columnName): ?LocalColumnMetadata
1358
    {
1359
        foreach ($this->declaredProperties as $property) {
1360
            if ($property instanceof LocalColumnMetadata && $property->getColumnName() === $columnName) {
1361
                return $property;
1362
            }
1363
        }
1364
1365
        return null;
1366
    }
1367
1368
    /**
1369
     * Add a property mapping.
1370 270
     *
1371
     * @param Property $property
1372 270
     *
1373
     * @throws \RuntimeException
1374 264
     * @throws MappingException
1375 194
     * @throws CacheException
1376
     */
1377
    public function addProperty(Property $property)
1378 264
    {
1379 260
        $fieldName = $property->getName();
1380
1381 86
        // Check for empty field name
1382
        if (empty($fieldName)) {
1383 86
            throw MappingException::missingFieldName($this->className);
1384 86
        }
1385
1386
        $property->setDeclaringClass($this);
1387
1388
        switch (true) {
1389 260
            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...
1390
                $this->validateAndCompleteFieldMapping($property);
1391 260
                $this->validateAndCompleteVersionFieldMapping($property);
1392 260
                break;
1393 140
1394 138
            case ($property instanceof FieldMetadata):
1395 138
                $this->validateAndCompleteFieldMapping($property);
1396
                break;
1397
1398 2
            case ($property instanceof OneToOneAssociationMetadata):
1399
                $this->validateAndCompleteAssociationMapping($property);
1400
                $this->validateAndCompleteToOneAssociationMetadata($property);
1401
                $this->validateAndCompleteOneToOneMapping($property);
1402 260
                break;
1403
1404 260
            case ($property instanceof OneToManyAssociationMetadata):
1405 33
                $this->validateAndCompleteAssociationMapping($property);
1406
                $this->validateAndCompleteToManyAssociationMetadata($property);
1407
                $this->validateAndCompleteOneToManyMapping($property);
1408 260
                break;
1409
1410
            case ($property instanceof ManyToOneAssociationMetadata):
1411
                $this->validateAndCompleteAssociationMapping($property);
1412 260
                $this->validateAndCompleteToOneAssociationMetadata($property);
1413 260
                $this->validateAndCompleteManyToOneMapping($property);
1414
                break;
1415 260
1416
            case ($property instanceof ManyToManyAssociationMetadata):
1417
                $this->validateAndCompleteAssociationMapping($property);
1418 260
                $this->validateAndCompleteToManyAssociationMetadata($property);
1419 2
                $this->validateAndCompleteManyToManyMapping($property);
1420
                break;
1421
1422
            default:
1423 2
                // Transient properties are ignored on purpose here! =)
1424 2
                break;
1425
        }
1426
1427
        $this->addDeclaredProperty($property);
1428 260
    }
1429
1430
    /**
1431 264
     * INTERNAL:
1432
     * Adds a property mapping without completing/validating it.
1433 264
     * This is mainly used to add inherited property mappings to derived classes.
1434 17
     *
1435 13
     * @param Property $property
1436
     *
1437
     * @return void
1438 17
     */
1439
    public function addInheritedProperty(Property $property)
1440
    {
1441 264
        $inheritedProperty = clone $property;
1442 2
        $declaringClass    = $property->getDeclaringClass();
1443
1444
        if ($inheritedProperty instanceof FieldMetadata) {
1445 262
            if (! $declaringClass->isMappedSuperclass) {
0 ignored issues
show
Bug introduced by
The property isMappedSuperclass does not seem to exist in Doctrine\ORM\Mapping\ComponentMetadata.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1446
                $inheritedProperty->setTableName($property->getTableName());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\ORM\Mapping\Property as the method getTableName() does only exist in the following implementations of said interface: Doctrine\ORM\Mapping\FieldMetadata, Doctrine\ORM\Mapping\VersionFieldMetadata.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1447
            }
1448
1449
            $this->fieldNames[$property->getColumnName()] = $property->getName();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\ORM\Mapping\Property as the method getColumnName() does only exist in the following implementations of said interface: Doctrine\ORM\Mapping\FieldMetadata, Doctrine\ORM\Mapping\VersionFieldMetadata.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1450
        } else if ($inheritedProperty instanceof AssociationMetadata) {
1451
            if ($declaringClass->isMappedSuperclass) {
1452
                $inheritedProperty->setSourceEntity($this->className);
1453
            }
1454
1455
            // Need to add inherited fieldNames
1456
            if ($inheritedProperty instanceof ToOneAssociationMetadata && $inheritedProperty->isOwningSide()) {
1457
                foreach ($inheritedProperty->getJoinColumns() as $joinColumn) {
1458 120
                    /** @var JoinColumnMetadata $joinColumn */
1459
                    $this->fieldNames[$joinColumn->getColumnName()] = $property->getName();
1460 120
                }
1461
            }
1462
        }
1463 119
1464
        if (isset($this->declaredProperties[$property->getName()])) {
1465
            throw MappingException::duplicateProperty($this->className, $this->getProperty($property->getName()));
0 ignored issues
show
Bug introduced by
It seems like $this->getProperty($property->getName()) can be null; however, duplicateProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1466
        }
1467 119
1468
        $this->declaredProperties[$property->getName()] = $inheritedProperty;
1469 119
1470 25
        if ($inheritedProperty instanceof VersionFieldMetadata) {
1471
            $this->versionProperty = $inheritedProperty;
1472
        }
1473 119
    }
1474 29
1475
    /**
1476
     * INTERNAL:
1477
     * Adds a named query to this class.
1478
     *
1479 119
     * @param string $name
1480
     * @param string $query
1481
     *
1482
     * @return void
1483
     *
1484
     * @throws MappingException
1485
     */
1486
    public function addNamedQuery(string $name, string $query)
1487
    {
1488
        if (isset($this->namedQueries[$name])) {
1489
            throw MappingException::duplicateQueryMapping($this->className, $name);
1490
        }
1491 128
1492
        $this->namedQueries[$name] = $query;
1493 128
    }
1494
1495 126
    /**
1496
     * INTERNAL:
1497 110
     * Adds a named native query to this class.
1498 20
     *
1499
     * @param string $name
1500
     * @param string $query
1501 110
     * @param array  $queryMapping
1502 110
     *
1503
     * @return void
1504 110
     *
1505 17
     * @throws MappingException
1506
     */
1507 17
    public function addNamedNativeQuery(string $name, string $query, array $queryMapping)
1508 17
    {
1509 17
        if (isset($this->namedNativeQueries[$name])) {
1510
            throw MappingException::duplicateQueryMapping($this->className, $name);
1511
        }
1512
1513
        if (! isset($queryMapping['resultClass']) && ! isset($queryMapping['resultSetMapping'])) {
1514 110
            throw MappingException::missingQueryMapping($this->className, $name);
1515 18
        }
1516
1517 18
        if (isset($queryMapping['resultClass']) && $queryMapping['resultClass'] !== '__CLASS__') {
1518 18
            $queryMapping['resultClass'] = $this->fullyQualifiedClassName($queryMapping['resultClass']);
1519 18
        }
1520
1521
        $this->namedNativeQueries[$name] = array_merge(['query' => $query], $queryMapping);
1522
    }
1523
1524 110
    /**
1525
     * INTERNAL:
1526 110
     * Adds a sql result set mapping to this class.
1527 110
     *
1528 2
     * @param array $resultMapping
1529
     *
1530
     * @return void
1531 110
     *
1532 6
     * @throws MappingException
1533
     */
1534
    public function addSqlResultSetMapping(array $resultMapping)
1535 110
    {
1536 26
        if (!isset($resultMapping['name'])) {
1537
            throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->className);
1538
        }
1539 110
1540 110
        if (isset($this->sqlResultSetMappings[$resultMapping['name']])) {
1541
            throw MappingException::duplicateResultSetMapping($this->className, $resultMapping['name']);
1542
        }
1543 110
1544 110
        if (isset($resultMapping['entities'])) {
1545 2
            foreach ($resultMapping['entities'] as $key => $entityResult) {
1546
                if (! isset($entityResult['entityClass'])) {
1547
                    throw MappingException::missingResultSetMappingEntity($this->className, $resultMapping['name']);
1548 110
                }
1549 6
1550
                $entityClassName = ($entityResult['entityClass'] !== '__CLASS__')
1551
                    ? $this->fullyQualifiedClassName($entityResult['entityClass'])
1552 110
                    : $entityResult['entityClass']
1553 22
                ;
1554
1555
                $resultMapping['entities'][$key]['entityClass'] = $entityClassName;
1556 110
1557 110
                if (isset($entityResult['fields'])) {
1558
                    foreach ($entityResult['fields'] as $k => $field) {
1559
                        if (! isset($field['name'])) {
1560
                            throw MappingException::missingResultSetMappingFieldName($this->className, $resultMapping['name']);
1561 126
                        }
1562
1563 126
                        if (! isset($field['column'])) {
1564 3
                            $fieldName = $field['name'];
1565
1566
                            if (strpos($fieldName, '.')) {
1567
                                list(, $fieldName) = explode('.', $fieldName);
1568
                            }
1569 126
1570
                            $resultMapping['entities'][$key]['fields'][$k]['column'] = $fieldName;
1571
                        }
1572
                    }
1573
                }
1574
            }
1575 603
        }
1576
1577 603
        $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping;
1578
    }
1579
1580
    /**
1581
     * Registers a custom repository class for the entity class.
1582
     *
1583
     * @param string|null $repositoryClassName The class name of the custom mapper.
1584
     *
1585
     * @return void
1586
     */
1587
    public function setCustomRepositoryClassName(?string $repositoryClassName)
1588 370
    {
1589
        $this->customRepositoryClassName = $repositoryClassName;
1590 370
    }
1591 1
1592
    /**
1593
     * @return string|null
1594 369
     */
1595
    public function getCustomRepositoryClassName() : ?string
1596
    {
1597
        return $this->customRepositoryClassName;
1598
    }
1599
1600
    /**
1601
     * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
1602
     *
1603
     * @param string $lifecycleEvent
1604
     *
1605
     * @return boolean
1606
     */
1607
    public function hasLifecycleCallbacks($lifecycleEvent)
1608
    {
1609
        return isset($this->lifecycleCallbacks[$lifecycleEvent]);
1610
    }
1611
1612
    /**
1613
     * Gets the registered lifecycle callbacks for an event.
1614
     *
1615
     * @param string $event
1616
     *
1617
     * @return array
1618
     */
1619 108
    public function getLifecycleCallbacks($event)
1620
    {
1621 108
        return isset($this->lifecycleCallbacks[$event]) ? $this->lifecycleCallbacks[$event] : [];
1622 108
    }
1623 108
1624
    /**
1625
     * Adds a lifecycle callback for entities of this class.
1626
     *
1627
     * @param string $callback
1628 51
     * @param string $event
1629
     *
1630 51
     * @return void
1631
     */
1632
    public function addLifecycleCallback($callback, $event)
1633
    {
1634
        if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
1635
            return;
1636 292
        }
1637
1638 292
        $this->lifecycleCallbacks[$event][] = $callback;
1639
    }
1640
1641
    /**
1642
     * Sets the lifecycle callbacks for entities of this class.
1643
     * Any previously registered callbacks are overwritten.
1644
     *
1645
     * @param array $callbacks
1646
     *
1647
     * @return void
1648 43
     */
1649
    public function setLifecycleCallbacks(array $callbacks) : void
1650 43
    {
1651 42
        $this->lifecycleCallbacks = $callbacks;
1652
    }
1653
1654 1
    /**
1655
     * Adds a entity listener for entities of this class.
1656
     *
1657
     * @param string $eventName The entity lifecycle event.
1658
     * @param string $class     The listener class.
1659
     * @param string $method    The listener callback method.
1660
     *
1661
     * @return void
1662 322
     *
1663
     * @throws \Doctrine\ORM\Mapping\MappingException
1664 322
     */
1665
    public function addEntityListener(string $eventName, string $class, string $method) : void
1666 322
    {
1667 322
        $listener = [
1668 318
            'class'  => $class,
1669
            'method' => $method,
1670 318
        ];
1671
1672
        if (! class_exists($class)) {
1673
            throw MappingException::entityListenerClassNotFound($class, $this->className);
1674 22
        }
1675 22
1676
        if (! method_exists($class, $method)) {
1677 22
            throw MappingException::entityListenerMethodNotFound($class, $method, $this->className);
1678 22
        }
1679 22
1680
        if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
1681 22
            throw MappingException::duplicateEntityListener($class, $method, $this->className);
1682
        }
1683 22
1684
        $this->entityListeners[$eventName][] = $listener;
1685
    }
1686
1687
    /**
1688 322
     * Sets the discriminator column definition.
1689
     *
1690
     * @param DiscriminatorColumnMetadata $discriminatorColumn
1691
     *
1692
     * @return void
1693
     *
1694
     * @throws MappingException
1695
     *
1696
     * @see getDiscriminatorColumn()
1697
     */
1698 397
    public function setDiscriminatorColumn(DiscriminatorColumnMetadata $discriminatorColumn) : void
1699
    {
1700 397
        if (isset($this->fieldNames[$discriminatorColumn->getColumnName()])) {
1701 397
            throw MappingException::duplicateColumnName($this->className, $discriminatorColumn->getColumnName());
1702
        }
1703
1704
        $discriminatorColumn->setTableName($discriminatorColumn->getTableName() ?? $this->getTableName());
1705
1706
        $allowedTypeList = ['boolean', 'array', 'object', 'datetime', 'time', 'date'];
1707
1708 373
        if (in_array($discriminatorColumn->getTypeName(), $allowedTypeList, true)) {
1709
            throw MappingException::invalidDiscriminatorColumnType($discriminatorColumn->getTypeName());
1710 373
        }
1711
1712
        $this->discriminatorColumn = $discriminatorColumn;
1713
    }
1714
1715
    /**
1716 1304
     * Sets the discriminator values used by this class.
1717
     * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
1718 1304
     *
1719
     * @param array $map
1720
     *
1721
     * @return void
1722
     *
1723
     * @throws MappingException
1724
     */
1725
    public function setDiscriminatorMap(array $map) : void
1726
    {
1727 1052
        foreach ($map as $value => $className) {
1728
            $this->addDiscriminatorMapClass($value, $className);
1729 1052
        }
1730
    }
1731
1732
    /**
1733
     * Adds one entry of the discriminator map with a new class and corresponding name.
1734
     *
1735
     * @param string|int $name
1736
     * @param string     $className
1737
     *
1738 1199
     * @return void
1739
     *
1740 1199
     * @throws MappingException
1741
     */
1742
    public function addDiscriminatorMapClass($name, string $className) : void
1743
    {
1744
        $this->discriminatorMap[$name] = $className;
1745
1746
        if ($this->className === $className) {
1747
            $this->discriminatorValue = $name;
1748
1749 260
            return;
1750
        }
1751 260
1752
        if (! (class_exists($className) || interface_exists($className))) {
1753
            throw MappingException::invalidClassInDiscriminatorMap($className, $this->className);
1754
        }
1755
1756
        if (is_subclass_of($className, $this->className) && ! in_array($className, $this->subClasses, true)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $this->className can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1757
            $this->subClasses[] = $className;
1758
        }
1759 1063
    }
1760
1761 1063
    /**
1762
     * @return ValueGenerationPlan
1763
     */
1764
    public function getValueGenerationPlan() : ValueGenerationPlan
1765
    {
1766
        return $this->valueGenerationPlan;
1767
    }
1768
1769 308
    /**
1770
     * @param ValueGenerationPlan $valueGenerationPlan
1771 308
     */
1772
    public function setValueGenerationPlan(ValueGenerationPlan $valueGenerationPlan) : void
1773
    {
1774
        $this->valueGenerationPlan = $valueGenerationPlan;
1775
    }
1776
1777
    /**
1778
     * Checks whether the class has a named query with the given query name.
1779 71
     *
1780
     * @param string $queryName
1781 71
     *
1782
     * @return boolean
1783
     */
1784
    public function hasNamedQuery($queryName) : bool
1785
    {
1786
        return isset($this->namedQueries[$queryName]);
1787
    }
1788
1789
    /**
1790 72
     * Checks whether the class has a named native query with the given query name.
1791
     *
1792 72
     * @param string $queryName
1793
     *
1794
     * @return boolean
1795
     */
1796
    public function hasNamedNativeQuery($queryName) : bool
1797
    {
1798
        return isset($this->namedNativeQueries[$queryName]);
1799
    }
1800
1801
    /**
1802
     * Checks whether the class has a named native query with the given query name.
1803
     *
1804
     * @param string $name
1805
     *
1806
     * @return boolean
1807
     */
1808
    public function hasSqlResultSetMapping($name) : bool
1809
    {
1810 1781
        return isset($this->sqlResultSetMappings[$name]);
1811
    }
1812 1781
1813
    /**
1814
     * Marks this class as read only, no change tracking is applied to it.
1815
     *
1816
     * @return void
1817
     */
1818
    public function asReadOnly() : void
1819
    {
1820 1382
        $this->readOnly = true;
1821
    }
1822 1382
1823
    /**
1824
     * Whether this class is read only or not.
1825
     *
1826
     * @return bool
1827
     */
1828
    public function isReadOnly() : bool
1829
    {
1830 7
        return $this->readOnly;
1831
    }
1832 7
1833 6
    /**
1834 7
     * @return bool
1835
     */
1836
    public function isVersioned() : bool
1837
    {
1838 7
        return $this->versionProperty !== null;
1839
    }
1840
1841
    /**
1842
     * Map Embedded Class
1843
     *
1844
     * @param array $mapping
1845
     *
1846
     * @throws MappingException
1847
     * @return void
1848 2
     */
1849
    public function mapEmbedded(array $mapping) : void
0 ignored issues
show
Unused Code introduced by
The parameter $mapping is not used and could be removed.

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

Loading history...
1850 2
    {
1851 2
        /*if (isset($this->declaredProperties[$mapping['fieldName']])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
69% 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...
1852
            throw MappingException::duplicateProperty($this->className, $this->getProperty($mapping['fieldName']));
1853 2
        }
1854
1855
        $this->embeddedClasses[$mapping['fieldName']] = [
1856
            'class'          => $this->fullyQualifiedClassName($mapping['class']),
1857
            'columnPrefix'   => $mapping['columnPrefix'],
1858
            'declaredField'  => isset($mapping['declaredField']) ? $mapping['declaredField'] : null,
1859
            'originalField'  => isset($mapping['originalField']) ? $mapping['originalField'] : null,
1860
            'declaringClass' => $this,
1861
        ];*/
1862
    }
1863
1864 391
    /**
1865
     * Inline the embeddable class
1866 391
     *
1867
     * @param string        $property
1868 391
     * @param ClassMetadata $embeddable
1869 75
     */
1870
    public function inlineEmbeddable($property, ClassMetadata $embeddable) : void
0 ignored issues
show
Unused Code introduced by
The parameter $property is not used and could be removed.

This check looks from 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 $embeddable is not used and could be removed.

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

Loading history...
1871 391
    {
1872
        /*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...
1873
            $fieldMapping['fieldName']     = $property . "." . $fieldName;
1874
            $fieldMapping['originalClass'] = $fieldMapping['originalClass'] ?? $embeddable->getClassName();
1875
            $fieldMapping['originalField'] = $fieldMapping['originalField'] ?? $fieldName;
1876
            $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
1877
                ? $property . '.' . $fieldMapping['declaredField']
1878
                : $property;
1879
1880
            if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
1881
                $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
1882 139
            } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
1883
                $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName(
1884 139
                    $property,
1885
                    $fieldMapping['columnName'],
1886
                    $this->reflectionClass->getName(),
1887
                    $embeddable->reflectionClass->getName()
1888 139
                );
1889 139
            }
1890
1891
            $this->mapField($fieldMapping);
1892
        }*/
1893
    }
1894
}
1895