summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Mapping/ClassMetadata.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Mapping/ClassMetadata.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Mapping/ClassMetadata.php')
-rw-r--r--vendor/doctrine/orm/src/Mapping/ClassMetadata.php2649
1 files changed, 2649 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Mapping/ClassMetadata.php b/vendor/doctrine/orm/src/Mapping/ClassMetadata.php
new file mode 100644
index 0000000..f58e00e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ClassMetadata.php
@@ -0,0 +1,2649 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use BackedEnum;
8use BadMethodCallException;
9use Doctrine\DBAL\Platforms\AbstractPlatform;
10use Doctrine\Deprecations\Deprecation;
11use Doctrine\Instantiator\Instantiator;
12use Doctrine\Instantiator\InstantiatorInterface;
13use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
14use Doctrine\ORM\EntityRepository;
15use Doctrine\ORM\Id\AbstractIdGenerator;
16use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
17use Doctrine\Persistence\Mapping\ReflectionService;
18use Doctrine\Persistence\Reflection\EnumReflectionProperty;
19use InvalidArgumentException;
20use LogicException;
21use ReflectionClass;
22use ReflectionNamedType;
23use ReflectionProperty;
24use Stringable;
25
26use function array_diff;
27use function array_intersect;
28use function array_key_exists;
29use function array_keys;
30use function array_map;
31use function array_merge;
32use function array_pop;
33use function array_values;
34use function assert;
35use function class_exists;
36use function count;
37use function enum_exists;
38use function explode;
39use function in_array;
40use function interface_exists;
41use function is_string;
42use function is_subclass_of;
43use function ltrim;
44use function method_exists;
45use function spl_object_id;
46use function sprintf;
47use function str_contains;
48use function str_replace;
49use function strtolower;
50use function trait_exists;
51use function trim;
52
53/**
54 * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
55 * of an entity and its associations.
56 *
57 * Once populated, ClassMetadata instances are usually cached in a serialized form.
58 *
59 * <b>IMPORTANT NOTE:</b>
60 *
61 * The fields of this class are only public for 2 reasons:
62 * 1) To allow fast READ access.
63 * 2) To drastically reduce the size of a serialized instance (private/protected members
64 * get the whole class name, namespace inclusive, prepended to every property in
65 * the serialized representation).
66 *
67 * @psalm-type ConcreteAssociationMapping = OneToOneOwningSideMapping|OneToOneInverseSideMapping|ManyToOneAssociationMapping|OneToManyAssociationMapping|ManyToManyOwningSideMapping|ManyToManyInverseSideMapping
68 * @template-covariant T of object
69 * @template-implements PersistenceClassMetadata<T>
70 */
71class ClassMetadata implements PersistenceClassMetadata, Stringable
72{
73 /* The inheritance mapping types */
74 /**
75 * NONE means the class does not participate in an inheritance hierarchy
76 * and therefore does not need an inheritance mapping type.
77 */
78 public const INHERITANCE_TYPE_NONE = 1;
79
80 /**
81 * JOINED means the class will be persisted according to the rules of
82 * <tt>Class Table Inheritance</tt>.
83 */
84 public const INHERITANCE_TYPE_JOINED = 2;
85
86 /**
87 * SINGLE_TABLE means the class will be persisted according to the rules of
88 * <tt>Single Table Inheritance</tt>.
89 */
90 public const INHERITANCE_TYPE_SINGLE_TABLE = 3;
91
92 /* The Id generator types. */
93 /**
94 * AUTO means the generator type will depend on what the used platform prefers.
95 * Offers full portability.
96 */
97 public const GENERATOR_TYPE_AUTO = 1;
98
99 /**
100 * SEQUENCE means a separate sequence object will be used. Platforms that do
101 * not have native sequence support may emulate it. Full portability is currently
102 * not guaranteed.
103 */
104 public const GENERATOR_TYPE_SEQUENCE = 2;
105
106 /**
107 * IDENTITY means an identity column is used for id generation. The database
108 * will fill in the id column on insertion. Platforms that do not support
109 * native identity columns may emulate them. Full portability is currently
110 * not guaranteed.
111 */
112 public const GENERATOR_TYPE_IDENTITY = 4;
113
114 /**
115 * NONE means the class does not have a generated id. That means the class
116 * must have a natural, manually assigned id.
117 */
118 public const GENERATOR_TYPE_NONE = 5;
119
120 /**
121 * CUSTOM means that customer will use own ID generator that supposedly work
122 */
123 public const GENERATOR_TYPE_CUSTOM = 7;
124
125 /**
126 * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
127 * by doing a property-by-property comparison with the original data. This will
128 * be done for all entities that are in MANAGED state at commit-time.
129 *
130 * This is the default change tracking policy.
131 */
132 public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
133
134 /**
135 * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
136 * by doing a property-by-property comparison with the original data. This will
137 * be done only for entities that were explicitly saved (through persist() or a cascade).
138 */
139 public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
140
141 /**
142 * Specifies that an association is to be fetched when it is first accessed.
143 */
144 public const FETCH_LAZY = 2;
145
146 /**
147 * Specifies that an association is to be fetched when the owner of the
148 * association is fetched.
149 */
150 public const FETCH_EAGER = 3;
151
152 /**
153 * Specifies that an association is to be fetched lazy (on first access) and that
154 * commands such as Collection#count, Collection#slice are issued directly against
155 * the database if the collection is not yet initialized.
156 */
157 public const FETCH_EXTRA_LAZY = 4;
158
159 /**
160 * Identifies a one-to-one association.
161 */
162 public const ONE_TO_ONE = 1;
163
164 /**
165 * Identifies a many-to-one association.
166 */
167 public const MANY_TO_ONE = 2;
168
169 /**
170 * Identifies a one-to-many association.
171 */
172 public const ONE_TO_MANY = 4;
173
174 /**
175 * Identifies a many-to-many association.
176 */
177 public const MANY_TO_MANY = 8;
178
179 /**
180 * Combined bitmask for to-one (single-valued) associations.
181 */
182 public const TO_ONE = 3;
183
184 /**
185 * Combined bitmask for to-many (collection-valued) associations.
186 */
187 public const TO_MANY = 12;
188
189 /**
190 * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks,
191 */
192 public const CACHE_USAGE_READ_ONLY = 1;
193
194 /**
195 * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes.
196 */
197 public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2;
198
199 /**
200 * Read Write Attempts to lock the entity before update/delete.
201 */
202 public const CACHE_USAGE_READ_WRITE = 3;
203
204 /**
205 * The value of this column is never generated by the database.
206 */
207 public const GENERATED_NEVER = 0;
208
209 /**
210 * The value of this column is generated by the database on INSERT, but not on UPDATE.
211 */
212 public const GENERATED_INSERT = 1;
213
214 /**
215 * The value of this column is generated by the database on both INSERT and UDPATE statements.
216 */
217 public const GENERATED_ALWAYS = 2;
218
219 /**
220 * READ-ONLY: The namespace the entity class is contained in.
221 *
222 * @todo Not really needed. Usage could be localized.
223 */
224 public string|null $namespace = null;
225
226 /**
227 * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance
228 * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same
229 * as {@link $name}.
230 *
231 * @psalm-var class-string
232 */
233 public string $rootEntityName;
234
235 /**
236 * READ-ONLY: The definition of custom generator. Only used for CUSTOM
237 * generator type
238 *
239 * The definition has the following structure:
240 * <code>
241 * array(
242 * 'class' => 'ClassName',
243 * )
244 * </code>
245 *
246 * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
247 * @var array<string, string>|null
248 */
249 public array|null $customGeneratorDefinition = null;
250
251 /**
252 * The name of the custom repository class used for the entity class.
253 * (Optional).
254 *
255 * @psalm-var ?class-string<EntityRepository>
256 */
257 public string|null $customRepositoryClassName = null;
258
259 /**
260 * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
261 */
262 public bool $isMappedSuperclass = false;
263
264 /**
265 * READ-ONLY: Whether this class describes the mapping of an embeddable class.
266 */
267 public bool $isEmbeddedClass = false;
268
269 /**
270 * READ-ONLY: The names of the parent <em>entity</em> classes (ancestors), starting with the
271 * nearest one and ending with the root entity class.
272 *
273 * @psalm-var list<class-string>
274 */
275 public array $parentClasses = [];
276
277 /**
278 * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all
279 * <em>entity</em> subclasses of this class. These may also be abstract classes.
280 *
281 * This list is used, for example, to enumerate all necessary tables in JTI when querying for root
282 * or subclass entities, or to gather all fields comprised in an entity inheritance tree.
283 *
284 * For classes that do not use STI/JTI, this list is empty.
285 *
286 * Implementation note:
287 *
288 * In PHP, there is no general way to discover all subclasses of a given class at runtime. For that
289 * reason, the list of classes given in the discriminator map at the root entity is considered
290 * authoritative. The discriminator map must contain all <em>concrete</em> classes that can
291 * appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract
292 * entity classes, users are not required to list such classes with a discriminator value.
293 *
294 * The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the
295 * root entity has been loaded.
296 *
297 * For subclasses of such root entities, the list can be reused/passed downwards, it only needs to
298 * be filtered accordingly (only keep remaining subclasses)
299 *
300 * @psalm-var list<class-string>
301 */
302 public array $subClasses = [];
303
304 /**
305 * READ-ONLY: The names of all embedded classes based on properties.
306 *
307 * @psalm-var array<string, EmbeddedClassMapping>
308 */
309 public array $embeddedClasses = [];
310
311 /**
312 * READ-ONLY: The field names of all fields that are part of the identifier/primary key
313 * of the mapped entity class.
314 *
315 * @psalm-var list<string>
316 */
317 public array $identifier = [];
318
319 /**
320 * READ-ONLY: The inheritance mapping type used by the class.
321 *
322 * @psalm-var self::INHERITANCE_TYPE_*
323 */
324 public int $inheritanceType = self::INHERITANCE_TYPE_NONE;
325
326 /**
327 * READ-ONLY: The Id generator type used by the class.
328 *
329 * @psalm-var self::GENERATOR_TYPE_*
330 */
331 public int $generatorType = self::GENERATOR_TYPE_NONE;
332
333 /**
334 * READ-ONLY: The field mappings of the class.
335 * Keys are field names and values are FieldMapping instances
336 *
337 * @var array<string, FieldMapping>
338 */
339 public array $fieldMappings = [];
340
341 /**
342 * READ-ONLY: An array of field names. Used to look up field names from column names.
343 * Keys are column names and values are field names.
344 *
345 * @psalm-var array<string, string>
346 */
347 public array $fieldNames = [];
348
349 /**
350 * READ-ONLY: A map of field names to column names. Keys are field names and values column names.
351 * Used to look up column names from field names.
352 * This is the reverse lookup map of $_fieldNames.
353 *
354 * @deprecated 3.0 Remove this.
355 *
356 * @var mixed[]
357 */
358 public array $columnNames = [];
359
360 /**
361 * READ-ONLY: The discriminator value of this class.
362 *
363 * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
364 * where a discriminator column is used.</b>
365 *
366 * @see discriminatorColumn
367 */
368 public mixed $discriminatorValue = null;
369
370 /**
371 * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
372 *
373 * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
374 * where a discriminator column is used.</b>
375 *
376 * @see discriminatorColumn
377 *
378 * @var array<int|string, string>
379 *
380 * @psalm-var array<int|string, class-string>
381 */
382 public array $discriminatorMap = [];
383
384 /**
385 * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
386 * inheritance mappings.
387 */
388 public DiscriminatorColumnMapping|null $discriminatorColumn = null;
389
390 /**
391 * READ-ONLY: The primary table definition. The definition is an array with the
392 * following entries:
393 *
394 * name => <tableName>
395 * schema => <schemaName>
396 * indexes => array
397 * uniqueConstraints => array
398 *
399 * @var mixed[]
400 * @psalm-var array{
401 * name: string,
402 * schema?: string,
403 * indexes?: array,
404 * uniqueConstraints?: array,
405 * options?: array<string, mixed>,
406 * quoted?: bool
407 * }
408 */
409 public array $table;
410
411 /**
412 * READ-ONLY: The registered lifecycle callbacks for entities of this class.
413 *
414 * @psalm-var array<string, list<string>>
415 */
416 public array $lifecycleCallbacks = [];
417
418 /**
419 * READ-ONLY: The registered entity listeners.
420 *
421 * @psalm-var array<string, list<array{class: class-string, method: string}>>
422 */
423 public array $entityListeners = [];
424
425 /**
426 * READ-ONLY: The association mappings of this class.
427 *
428 * A join table definition has the following structure:
429 * <pre>
430 * array(
431 * 'name' => <join table name>,
432 * 'joinColumns' => array(<join column mapping from join table to source table>),
433 * 'inverseJoinColumns' => array(<join column mapping from join table to target table>)
434 * )
435 * </pre>
436 *
437 * @psalm-var array<string, ConcreteAssociationMapping>
438 */
439 public array $associationMappings = [];
440
441 /**
442 * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
443 */
444 public bool $isIdentifierComposite = false;
445
446 /**
447 * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
448 *
449 * This flag is necessary because some code blocks require special treatment of this cases.
450 */
451 public bool $containsForeignIdentifier = false;
452
453 /**
454 * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
455 *
456 * This flag is necessary because some code blocks require special treatment of this cases.
457 */
458 public bool $containsEnumIdentifier = false;
459
460 /**
461 * READ-ONLY: The ID generator used for generating IDs for this class.
462 *
463 * @todo Remove!
464 */
465 public AbstractIdGenerator $idGenerator;
466
467 /**
468 * READ-ONLY: The definition of the sequence generator of this class. Only used for the
469 * SEQUENCE generation strategy.
470 *
471 * The definition has the following structure:
472 * <code>
473 * array(
474 * 'sequenceName' => 'name',
475 * 'allocationSize' => '20',
476 * 'initialValue' => '1'
477 * )
478 * </code>
479 *
480 * @var array<string, mixed>|null
481 * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
482 * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
483 */
484 public array|null $sequenceGeneratorDefinition = null;
485
486 /**
487 * READ-ONLY: The policy used for change-tracking on entities of this class.
488 */
489 public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
490
491 /**
492 * READ-ONLY: A Flag indicating whether one or more columns of this class
493 * have to be reloaded after insert / update operations.
494 */
495 public bool $requiresFetchAfterChange = false;
496
497 /**
498 * READ-ONLY: A flag for whether or not instances of this class are to be versioned
499 * with optimistic locking.
500 */
501 public bool $isVersioned = false;
502
503 /**
504 * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
505 */
506 public string|null $versionField = null;
507
508 /** @var mixed[]|null */
509 public array|null $cache = null;
510
511 /**
512 * The ReflectionClass instance of the mapped class.
513 *
514 * @var ReflectionClass<T>|null
515 */
516 public ReflectionClass|null $reflClass = null;
517
518 /**
519 * Is this entity marked as "read-only"?
520 *
521 * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
522 * optimization for entities that are immutable, either in your domain or through the relation database
523 * (coming from a view, or a history table for example).
524 */
525 public bool $isReadOnly = false;
526
527 /**
528 * NamingStrategy determining the default column and table names.
529 */
530 protected NamingStrategy $namingStrategy;
531
532 /**
533 * The ReflectionProperty instances of the mapped class.
534 *
535 * @var array<string, ReflectionProperty|null>
536 */
537 public array $reflFields = [];
538
539 private InstantiatorInterface|null $instantiator = null;
540
541 private readonly TypedFieldMapper $typedFieldMapper;
542
543 /**
544 * Initializes a new ClassMetadata instance that will hold the object-relational mapping
545 * metadata of the class with the given name.
546 *
547 * @param string $name The name of the entity class the new instance is used for.
548 * @psalm-param class-string<T> $name
549 */
550 public function __construct(public string $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null)
551 {
552 $this->rootEntityName = $name;
553 $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy();
554 $this->instantiator = new Instantiator();
555 $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper();
556 }
557
558 /**
559 * Gets the ReflectionProperties of the mapped class.
560 *
561 * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
562 * @psalm-return array<ReflectionProperty|null>
563 */
564 public function getReflectionProperties(): array
565 {
566 return $this->reflFields;
567 }
568
569 /**
570 * Gets a ReflectionProperty for a specific field of the mapped class.
571 */
572 public function getReflectionProperty(string $name): ReflectionProperty|null
573 {
574 return $this->reflFields[$name];
575 }
576
577 /**
578 * Gets the ReflectionProperty for the single identifier field.
579 *
580 * @throws BadMethodCallException If the class has a composite identifier.
581 */
582 public function getSingleIdReflectionProperty(): ReflectionProperty|null
583 {
584 if ($this->isIdentifierComposite) {
585 throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
586 }
587
588 return $this->reflFields[$this->identifier[0]];
589 }
590
591 /**
592 * Extracts the identifier values of an entity of this class.
593 *
594 * For composite identifiers, the identifier values are returned as an array
595 * with the same order as the field order in {@link identifier}.
596 *
597 * @return array<string, mixed>
598 */
599 public function getIdentifierValues(object $entity): array
600 {
601 if ($this->isIdentifierComposite) {
602 $id = [];
603
604 foreach ($this->identifier as $idField) {
605 $value = $this->reflFields[$idField]->getValue($entity);
606
607 if ($value !== null) {
608 $id[$idField] = $value;
609 }
610 }
611
612 return $id;
613 }
614
615 $id = $this->identifier[0];
616 $value = $this->reflFields[$id]->getValue($entity);
617
618 if ($value === null) {
619 return [];
620 }
621
622 return [$id => $value];
623 }
624
625 /**
626 * Populates the entity identifier of an entity.
627 *
628 * @psalm-param array<string, mixed> $id
629 *
630 * @todo Rename to assignIdentifier()
631 */
632 public function setIdentifierValues(object $entity, array $id): void
633 {
634 foreach ($id as $idField => $idValue) {
635 $this->reflFields[$idField]->setValue($entity, $idValue);
636 }
637 }
638
639 /**
640 * Sets the specified field to the specified value on the given entity.
641 */
642 public function setFieldValue(object $entity, string $field, mixed $value): void
643 {
644 $this->reflFields[$field]->setValue($entity, $value);
645 }
646
647 /**
648 * Gets the specified field's value off the given entity.
649 */
650 public function getFieldValue(object $entity, string $field): mixed
651 {
652 return $this->reflFields[$field]->getValue($entity);
653 }
654
655 /**
656 * Creates a string representation of this instance.
657 *
658 * @return string The string representation of this instance.
659 *
660 * @todo Construct meaningful string representation.
661 */
662 public function __toString(): string
663 {
664 return self::class . '@' . spl_object_id($this);
665 }
666
667 /**
668 * Determines which fields get serialized.
669 *
670 * It is only serialized what is necessary for best unserialization performance.
671 * That means any metadata properties that are not set or empty or simply have
672 * their default value are NOT serialized.
673 *
674 * Parts that are also NOT serialized because they can not be properly unserialized:
675 * - reflClass (ReflectionClass)
676 * - reflFields (ReflectionProperty array)
677 *
678 * @return string[] The names of all the fields that should be serialized.
679 */
680 public function __sleep(): array
681 {
682 // This metadata is always serialized/cached.
683 $serialized = [
684 'associationMappings',
685 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
686 'fieldMappings',
687 'fieldNames',
688 'embeddedClasses',
689 'identifier',
690 'isIdentifierComposite', // TODO: REMOVE
691 'name',
692 'namespace', // TODO: REMOVE
693 'table',
694 'rootEntityName',
695 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
696 ];
697
698 // The rest of the metadata is only serialized if necessary.
699 if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
700 $serialized[] = 'changeTrackingPolicy';
701 }
702
703 if ($this->customRepositoryClassName) {
704 $serialized[] = 'customRepositoryClassName';
705 }
706
707 if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) {
708 $serialized[] = 'inheritanceType';
709 $serialized[] = 'discriminatorColumn';
710 $serialized[] = 'discriminatorValue';
711 $serialized[] = 'discriminatorMap';
712 $serialized[] = 'parentClasses';
713 $serialized[] = 'subClasses';
714 }
715
716 if ($this->generatorType !== self::GENERATOR_TYPE_NONE) {
717 $serialized[] = 'generatorType';
718 if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) {
719 $serialized[] = 'sequenceGeneratorDefinition';
720 }
721 }
722
723 if ($this->isMappedSuperclass) {
724 $serialized[] = 'isMappedSuperclass';
725 }
726
727 if ($this->isEmbeddedClass) {
728 $serialized[] = 'isEmbeddedClass';
729 }
730
731 if ($this->containsForeignIdentifier) {
732 $serialized[] = 'containsForeignIdentifier';
733 }
734
735 if ($this->containsEnumIdentifier) {
736 $serialized[] = 'containsEnumIdentifier';
737 }
738
739 if ($this->isVersioned) {
740 $serialized[] = 'isVersioned';
741 $serialized[] = 'versionField';
742 }
743
744 if ($this->lifecycleCallbacks) {
745 $serialized[] = 'lifecycleCallbacks';
746 }
747
748 if ($this->entityListeners) {
749 $serialized[] = 'entityListeners';
750 }
751
752 if ($this->isReadOnly) {
753 $serialized[] = 'isReadOnly';
754 }
755
756 if ($this->customGeneratorDefinition) {
757 $serialized[] = 'customGeneratorDefinition';
758 }
759
760 if ($this->cache) {
761 $serialized[] = 'cache';
762 }
763
764 if ($this->requiresFetchAfterChange) {
765 $serialized[] = 'requiresFetchAfterChange';
766 }
767
768 return $serialized;
769 }
770
771 /**
772 * Creates a new instance of the mapped class, without invoking the constructor.
773 */
774 public function newInstance(): object
775 {
776 return $this->instantiator->instantiate($this->name);
777 }
778
779 /**
780 * Restores some state that can not be serialized/unserialized.
781 */
782 public function wakeupReflection(ReflectionService $reflService): void
783 {
784 // Restore ReflectionClass and properties
785 $this->reflClass = $reflService->getClass($this->name);
786 $this->instantiator = $this->instantiator ?: new Instantiator();
787
788 $parentReflFields = [];
789
790 foreach ($this->embeddedClasses as $property => $embeddedClass) {
791 if (isset($embeddedClass->declaredField)) {
792 assert($embeddedClass->originalField !== null);
793 $childProperty = $this->getAccessibleProperty(
794 $reflService,
795 $this->embeddedClasses[$embeddedClass->declaredField]->class,
796 $embeddedClass->originalField,
797 );
798 assert($childProperty !== null);
799 $parentReflFields[$property] = new ReflectionEmbeddedProperty(
800 $parentReflFields[$embeddedClass->declaredField],
801 $childProperty,
802 $this->embeddedClasses[$embeddedClass->declaredField]->class,
803 );
804
805 continue;
806 }
807
808 $fieldRefl = $this->getAccessibleProperty(
809 $reflService,
810 $embeddedClass->declared ?? $this->name,
811 $property,
812 );
813
814 $parentReflFields[$property] = $fieldRefl;
815 $this->reflFields[$property] = $fieldRefl;
816 }
817
818 foreach ($this->fieldMappings as $field => $mapping) {
819 if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) {
820 assert($mapping->originalField !== null);
821 assert($mapping->originalClass !== null);
822 $childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField);
823 assert($childProperty !== null);
824
825 if (isset($mapping->enumType)) {
826 $childProperty = new EnumReflectionProperty(
827 $childProperty,
828 $mapping->enumType,
829 );
830 }
831
832 $this->reflFields[$field] = new ReflectionEmbeddedProperty(
833 $parentReflFields[$mapping->declaredField],
834 $childProperty,
835 $mapping->originalClass,
836 );
837 continue;
838 }
839
840 $this->reflFields[$field] = isset($mapping->declared)
841 ? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
842 : $this->getAccessibleProperty($reflService, $this->name, $field);
843
844 if (isset($mapping->enumType) && $this->reflFields[$field] !== null) {
845 $this->reflFields[$field] = new EnumReflectionProperty(
846 $this->reflFields[$field],
847 $mapping->enumType,
848 );
849 }
850 }
851
852 foreach ($this->associationMappings as $field => $mapping) {
853 $this->reflFields[$field] = isset($mapping->declared)
854 ? $this->getAccessibleProperty($reflService, $mapping->declared, $field)
855 : $this->getAccessibleProperty($reflService, $this->name, $field);
856 }
857 }
858
859 /**
860 * Initializes a new ClassMetadata instance that will hold the object-relational mapping
861 * metadata of the class with the given name.
862 *
863 * @param ReflectionService $reflService The reflection service.
864 */
865 public function initializeReflection(ReflectionService $reflService): void
866 {
867 $this->reflClass = $reflService->getClass($this->name);
868 $this->namespace = $reflService->getClassNamespace($this->name);
869
870 if ($this->reflClass) {
871 $this->name = $this->rootEntityName = $this->reflClass->name;
872 }
873
874 $this->table['name'] = $this->namingStrategy->classToTableName($this->name);
875 }
876
877 /**
878 * Validates Identifier.
879 *
880 * @throws MappingException
881 */
882 public function validateIdentifier(): void
883 {
884 if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
885 return;
886 }
887
888 // Verify & complete identifier mapping
889 if (! $this->identifier) {
890 throw MappingException::identifierRequired($this->name);
891 }
892
893 if ($this->usesIdGenerator() && $this->isIdentifierComposite) {
894 throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name);
895 }
896 }
897
898 /**
899 * Validates association targets actually exist.
900 *
901 * @throws MappingException
902 */
903 public function validateAssociations(): void
904 {
905 foreach ($this->associationMappings as $mapping) {
906 if (
907 ! class_exists($mapping->targetEntity)
908 && ! interface_exists($mapping->targetEntity)
909 && ! trait_exists($mapping->targetEntity)
910 ) {
911 throw MappingException::invalidTargetEntityClass($mapping->targetEntity, $this->name, $mapping->fieldName);
912 }
913 }
914 }
915
916 /**
917 * Validates lifecycle callbacks.
918 *
919 * @throws MappingException
920 */
921 public function validateLifecycleCallbacks(ReflectionService $reflService): void
922 {
923 foreach ($this->lifecycleCallbacks as $callbacks) {
924 foreach ($callbacks as $callbackFuncName) {
925 if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
926 throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
927 }
928 }
929 }
930 }
931
932 /**
933 * {@inheritDoc}
934 *
935 * Can return null when using static reflection, in violation of the LSP
936 */
937 public function getReflectionClass(): ReflectionClass|null
938 {
939 return $this->reflClass;
940 }
941
942 /** @psalm-param array{usage?: mixed, region?: mixed} $cache */
943 public function enableCache(array $cache): void
944 {
945 if (! isset($cache['usage'])) {
946 $cache['usage'] = self::CACHE_USAGE_READ_ONLY;
947 }
948
949 if (! isset($cache['region'])) {
950 $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
951 }
952
953 $this->cache = $cache;
954 }
955
956 /** @psalm-param array{usage?: int, region?: string} $cache */
957 public function enableAssociationCache(string $fieldName, array $cache): void
958 {
959 $this->associationMappings[$fieldName]->cache = $this->getAssociationCacheDefaults($fieldName, $cache);
960 }
961
962 /**
963 * @psalm-param array{usage?: int, region?: string|null} $cache
964 *
965 * @return int[]|string[]
966 * @psalm-return array{usage: int, region: string|null}
967 */
968 public function getAssociationCacheDefaults(string $fieldName, array $cache): array
969 {
970 if (! isset($cache['usage'])) {
971 $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY;
972 }
973
974 if (! isset($cache['region'])) {
975 $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
976 }
977
978 return $cache;
979 }
980
981 /**
982 * Sets the change tracking policy used by this class.
983 */
984 public function setChangeTrackingPolicy(int $policy): void
985 {
986 $this->changeTrackingPolicy = $policy;
987 }
988
989 /**
990 * Whether the change tracking policy of this class is "deferred explicit".
991 */
992 public function isChangeTrackingDeferredExplicit(): bool
993 {
994 return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
995 }
996
997 /**
998 * Whether the change tracking policy of this class is "deferred implicit".
999 */
1000 public function isChangeTrackingDeferredImplicit(): bool
1001 {
1002 return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
1003 }
1004
1005 /**
1006 * Checks whether a field is part of the identifier/primary key field(s).
1007 */
1008 public function isIdentifier(string $fieldName): bool
1009 {
1010 if (! $this->identifier) {
1011 return false;
1012 }
1013
1014 if (! $this->isIdentifierComposite) {
1015 return $fieldName === $this->identifier[0];
1016 }
1017
1018 return in_array($fieldName, $this->identifier, true);
1019 }
1020
1021 public function isUniqueField(string $fieldName): bool
1022 {
1023 $mapping = $this->getFieldMapping($fieldName);
1024
1025 return $mapping !== false && isset($mapping->unique) && $mapping->unique;
1026 }
1027
1028 public function isNullable(string $fieldName): bool
1029 {
1030 $mapping = $this->getFieldMapping($fieldName);
1031
1032 return $mapping !== false && isset($mapping->nullable) && $mapping->nullable;
1033 }
1034
1035 /**
1036 * Gets a column name for a field name.
1037 * If the column name for the field cannot be found, the given field name
1038 * is returned.
1039 */
1040 public function getColumnName(string $fieldName): string
1041 {
1042 return $this->columnNames[$fieldName] ?? $fieldName;
1043 }
1044
1045 /**
1046 * Gets the mapping of a (regular) field that holds some data but not a
1047 * reference to another object.
1048 *
1049 * @throws MappingException
1050 */
1051 public function getFieldMapping(string $fieldName): FieldMapping
1052 {
1053 if (! isset($this->fieldMappings[$fieldName])) {
1054 throw MappingException::mappingNotFound($this->name, $fieldName);
1055 }
1056
1057 return $this->fieldMappings[$fieldName];
1058 }
1059
1060 /**
1061 * Gets the mapping of an association.
1062 *
1063 * @see ClassMetadata::$associationMappings
1064 *
1065 * @param string $fieldName The field name that represents the association in
1066 * the object model.
1067 *
1068 * @throws MappingException
1069 */
1070 public function getAssociationMapping(string $fieldName): AssociationMapping
1071 {
1072 if (! isset($this->associationMappings[$fieldName])) {
1073 throw MappingException::mappingNotFound($this->name, $fieldName);
1074 }
1075
1076 return $this->associationMappings[$fieldName];
1077 }
1078
1079 /**
1080 * Gets all association mappings of the class.
1081 *
1082 * @psalm-return array<string, AssociationMapping>
1083 */
1084 public function getAssociationMappings(): array
1085 {
1086 return $this->associationMappings;
1087 }
1088
1089 /**
1090 * Gets the field name for a column name.
1091 * If no field name can be found the column name is returned.
1092 *
1093 * @return string The column alias.
1094 */
1095 public function getFieldName(string $columnName): string
1096 {
1097 return $this->fieldNames[$columnName] ?? $columnName;
1098 }
1099
1100 /**
1101 * Checks whether given property has type
1102 */
1103 private function isTypedProperty(string $name): bool
1104 {
1105 return isset($this->reflClass)
1106 && $this->reflClass->hasProperty($name)
1107 && $this->reflClass->getProperty($name)->hasType();
1108 }
1109
1110 /**
1111 * Validates & completes the given field mapping based on typed property.
1112 *
1113 * @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete.
1114 *
1115 * @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
1116 */
1117 private function validateAndCompleteTypedFieldMapping(array $mapping): array
1118 {
1119 $field = $this->reflClass->getProperty($mapping['fieldName']);
1120
1121 $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
1122
1123 return $mapping;
1124 }
1125
1126 /**
1127 * Validates & completes the basic mapping information based on typed property.
1128 *
1129 * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping.
1130 *
1131 * @return mixed[] The updated mapping.
1132 */
1133 private function validateAndCompleteTypedAssociationMapping(array $mapping): array
1134 {
1135 $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
1136
1137 if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) {
1138 return $mapping;
1139 }
1140
1141 if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) {
1142 $mapping['targetEntity'] = $type->getName();
1143 }
1144
1145 return $mapping;
1146 }
1147
1148 /**
1149 * Validates & completes the given field mapping.
1150 *
1151 * @psalm-param array{
1152 * fieldName?: string,
1153 * columnName?: string,
1154 * id?: bool,
1155 * generated?: self::GENERATED_*,
1156 * enumType?: class-string,
1157 * } $mapping The field mapping to validate & complete.
1158 *
1159 * @return FieldMapping The updated mapping.
1160 *
1161 * @throws MappingException
1162 */
1163 protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
1164 {
1165 // Check mandatory fields
1166 if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
1167 throw MappingException::missingFieldName($this->name);
1168 }
1169
1170 if ($this->isTypedProperty($mapping['fieldName'])) {
1171 $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
1172 }
1173
1174 if (! isset($mapping['type'])) {
1175 // Default to string
1176 $mapping['type'] = 'string';
1177 }
1178
1179 // Complete fieldName and columnName mapping
1180 if (! isset($mapping['columnName'])) {
1181 $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
1182 }
1183
1184 $mapping = FieldMapping::fromMappingArray($mapping);
1185
1186 if ($mapping->columnName[0] === '`') {
1187 $mapping->columnName = trim($mapping->columnName, '`');
1188 $mapping->quoted = true;
1189 }
1190
1191 $this->columnNames[$mapping->fieldName] = $mapping->columnName;
1192
1193 if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) {
1194 throw MappingException::duplicateColumnName($this->name, $mapping->columnName);
1195 }
1196
1197 $this->fieldNames[$mapping->columnName] = $mapping->fieldName;
1198
1199 // Complete id mapping
1200 if (isset($mapping->id) && $mapping->id === true) {
1201 if ($this->versionField === $mapping->fieldName) {
1202 throw MappingException::cannotVersionIdField($this->name, $mapping->fieldName);
1203 }
1204
1205 if (! in_array($mapping->fieldName, $this->identifier, true)) {
1206 $this->identifier[] = $mapping->fieldName;
1207 }
1208
1209 // Check for composite key
1210 if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
1211 $this->isIdentifierComposite = true;
1212 }
1213 }
1214
1215 if (isset($mapping->generated)) {
1216 if (! in_array($mapping->generated, [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
1217 throw MappingException::invalidGeneratedMode($mapping->generated);
1218 }
1219
1220 if ($mapping->generated === self::GENERATED_NEVER) {
1221 unset($mapping->generated);
1222 }
1223 }
1224
1225 if (isset($mapping->enumType)) {
1226 if (! enum_exists($mapping->enumType)) {
1227 throw MappingException::nonEnumTypeMapped($this->name, $mapping->fieldName, $mapping->enumType);
1228 }
1229
1230 if (! empty($mapping->id)) {
1231 $this->containsEnumIdentifier = true;
1232 }
1233 }
1234
1235 return $mapping;
1236 }
1237
1238 /**
1239 * Validates & completes the basic mapping information that is common to all
1240 * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
1241 *
1242 * @psalm-param array<string, mixed> $mapping The mapping.
1243 *
1244 * @return ConcreteAssociationMapping
1245 *
1246 * @throws MappingException If something is wrong with the mapping.
1247 */
1248 protected function _validateAndCompleteAssociationMapping(array $mapping): AssociationMapping
1249 {
1250 if (array_key_exists('mappedBy', $mapping) && $mapping['mappedBy'] === null) {
1251 unset($mapping['mappedBy']);
1252 }
1253
1254 if (array_key_exists('inversedBy', $mapping) && $mapping['inversedBy'] === null) {
1255 unset($mapping['inversedBy']);
1256 }
1257
1258 if (array_key_exists('joinColumns', $mapping) && in_array($mapping['joinColumns'], [null, []], true)) {
1259 unset($mapping['joinColumns']);
1260 }
1261
1262 $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
1263
1264 if (empty($mapping['indexBy'])) {
1265 unset($mapping['indexBy']);
1266 }
1267
1268 // If targetEntity is unqualified, assume it is in the same namespace as
1269 // the sourceEntity.
1270 $mapping['sourceEntity'] = $this->name;
1271
1272 if ($this->isTypedProperty($mapping['fieldName'])) {
1273 $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
1274 }
1275
1276 if (isset($mapping['targetEntity'])) {
1277 $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
1278 $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
1279 }
1280
1281 if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
1282 throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']);
1283 }
1284
1285 // Complete id mapping
1286 if (isset($mapping['id']) && $mapping['id'] === true) {
1287 if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
1288 throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
1289 }
1290
1291 if (! in_array($mapping['fieldName'], $this->identifier, true)) {
1292 if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) {
1293 throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
1294 $mapping['targetEntity'],
1295 $this->name,
1296 $mapping['fieldName'],
1297 );
1298 }
1299
1300 assert(is_string($mapping['fieldName']));
1301 $this->identifier[] = $mapping['fieldName'];
1302 $this->containsForeignIdentifier = true;
1303 }
1304
1305 // Check for composite key
1306 if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
1307 $this->isIdentifierComposite = true;
1308 }
1309
1310 if ($this->cache && ! isset($mapping['cache'])) {
1311 throw NonCacheableEntityAssociation::fromEntityAndField(
1312 $this->name,
1313 $mapping['fieldName'],
1314 );
1315 }
1316 }
1317
1318 // Mandatory attributes for both sides
1319 // Mandatory: fieldName, targetEntity
1320 if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
1321 throw MappingException::missingFieldName($this->name);
1322 }
1323
1324 if (! isset($mapping['targetEntity'])) {
1325 throw MappingException::missingTargetEntity($mapping['fieldName']);
1326 }
1327
1328 // Mandatory and optional attributes for either side
1329 if (! isset($mapping['mappedBy'])) {
1330 if (isset($mapping['joinTable'])) {
1331 if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') {
1332 $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`');
1333 $mapping['joinTable']['quoted'] = true;
1334 }
1335 }
1336 } else {
1337 $mapping['isOwningSide'] = false;
1338 }
1339
1340 if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
1341 throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']);
1342 }
1343
1344 // Fetch mode. Default fetch mode to LAZY, if not set.
1345 if (! isset($mapping['fetch'])) {
1346 $mapping['fetch'] = self::FETCH_LAZY;
1347 }
1348
1349 // Cascades
1350 $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
1351
1352 $allCascades = ['remove', 'persist', 'refresh', 'detach'];
1353 if (in_array('all', $cascades, true)) {
1354 $cascades = $allCascades;
1355 } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
1356 throw MappingException::invalidCascadeOption(
1357 array_diff($cascades, $allCascades),
1358 $this->name,
1359 $mapping['fieldName'],
1360 );
1361 }
1362
1363 $mapping['cascade'] = $cascades;
1364
1365 switch ($mapping['type']) {
1366 case self::ONE_TO_ONE:
1367 if (isset($mapping['joinColumns']) && $mapping['joinColumns'] && ! $mapping['isOwningSide']) {
1368 throw MappingException::joinColumnNotAllowedOnOneToOneInverseSide(
1369 $this->name,
1370 $mapping['fieldName'],
1371 );
1372 }
1373
1374 return $mapping['isOwningSide'] ?
1375 OneToOneOwningSideMapping::fromMappingArrayAndName(
1376 $mapping,
1377 $this->namingStrategy,
1378 $this->name,
1379 $this->table ?? null,
1380 $this->isInheritanceTypeSingleTable(),
1381 ) :
1382 OneToOneInverseSideMapping::fromMappingArrayAndName($mapping, $this->name);
1383
1384 case self::MANY_TO_ONE:
1385 return ManyToOneAssociationMapping::fromMappingArrayAndName(
1386 $mapping,
1387 $this->namingStrategy,
1388 $this->name,
1389 $this->table ?? null,
1390 $this->isInheritanceTypeSingleTable(),
1391 );
1392
1393 case self::ONE_TO_MANY:
1394 return OneToManyAssociationMapping::fromMappingArrayAndName($mapping, $this->name);
1395
1396 case self::MANY_TO_MANY:
1397 if (isset($mapping['joinColumns'])) {
1398 unset($mapping['joinColumns']);
1399 }
1400
1401 return $mapping['isOwningSide'] ?
1402 ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy($mapping, $this->namingStrategy) :
1403 ManyToManyInverseSideMapping::fromMappingArray($mapping);
1404
1405 default:
1406 throw MappingException::invalidAssociationType(
1407 $this->name,
1408 $mapping['fieldName'],
1409 $mapping['type'],
1410 );
1411 }
1412 }
1413
1414 /**
1415 * {@inheritDoc}
1416 */
1417 public function getIdentifierFieldNames(): array
1418 {
1419 return $this->identifier;
1420 }
1421
1422 /**
1423 * Gets the name of the single id field. Note that this only works on
1424 * entity classes that have a single-field pk.
1425 *
1426 * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
1427 */
1428 public function getSingleIdentifierFieldName(): string
1429 {
1430 if ($this->isIdentifierComposite) {
1431 throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name);
1432 }
1433
1434 if (! isset($this->identifier[0])) {
1435 throw MappingException::noIdDefined($this->name);
1436 }
1437
1438 return $this->identifier[0];
1439 }
1440
1441 /**
1442 * Gets the column name of the single id column. Note that this only works on
1443 * entity classes that have a single-field pk.
1444 *
1445 * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
1446 */
1447 public function getSingleIdentifierColumnName(): string
1448 {
1449 return $this->getColumnName($this->getSingleIdentifierFieldName());
1450 }
1451
1452 /**
1453 * INTERNAL:
1454 * Sets the mapped identifier/primary key fields of this class.
1455 * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
1456 *
1457 * @psalm-param list<mixed> $identifier
1458 */
1459 public function setIdentifier(array $identifier): void
1460 {
1461 $this->identifier = $identifier;
1462 $this->isIdentifierComposite = (count($this->identifier) > 1);
1463 }
1464
1465 /**
1466 * {@inheritDoc}
1467 */
1468 public function getIdentifier(): array
1469 {
1470 return $this->identifier;
1471 }
1472
1473 public function hasField(string $fieldName): bool
1474 {
1475 return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]);
1476 }
1477
1478 /**
1479 * Gets an array containing all the column names.
1480 *
1481 * @psalm-param list<string>|null $fieldNames
1482 *
1483 * @return mixed[]
1484 * @psalm-return list<string>
1485 */
1486 public function getColumnNames(array|null $fieldNames = null): array
1487 {
1488 if ($fieldNames === null) {
1489 return array_keys($this->fieldNames);
1490 }
1491
1492 return array_values(array_map($this->getColumnName(...), $fieldNames));
1493 }
1494
1495 /**
1496 * Returns an array with all the identifier column names.
1497 *
1498 * @psalm-return list<string>
1499 */
1500 public function getIdentifierColumnNames(): array
1501 {
1502 $columnNames = [];
1503
1504 foreach ($this->identifier as $idProperty) {
1505 if (isset($this->fieldMappings[$idProperty])) {
1506 $columnNames[] = $this->fieldMappings[$idProperty]->columnName;
1507
1508 continue;
1509 }
1510
1511 // Association defined as Id field
1512 assert($this->associationMappings[$idProperty]->isToOneOwningSide());
1513 $joinColumns = $this->associationMappings[$idProperty]->joinColumns;
1514 $assocColumnNames = array_map(static fn (JoinColumnMapping $joinColumn): string => $joinColumn->name, $joinColumns);
1515
1516 $columnNames = array_merge($columnNames, $assocColumnNames);
1517 }
1518
1519 return $columnNames;
1520 }
1521
1522 /**
1523 * Sets the type of Id generator to use for the mapped class.
1524 *
1525 * @psalm-param self::GENERATOR_TYPE_* $generatorType
1526 */
1527 public function setIdGeneratorType(int $generatorType): void
1528 {
1529 $this->generatorType = $generatorType;
1530 }
1531
1532 /**
1533 * Checks whether the mapped class uses an Id generator.
1534 */
1535 public function usesIdGenerator(): bool
1536 {
1537 return $this->generatorType !== self::GENERATOR_TYPE_NONE;
1538 }
1539
1540 public function isInheritanceTypeNone(): bool
1541 {
1542 return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
1543 }
1544
1545 /**
1546 * Checks whether the mapped class uses the JOINED inheritance mapping strategy.
1547 *
1548 * @return bool TRUE if the class participates in a JOINED inheritance mapping,
1549 * FALSE otherwise.
1550 */
1551 public function isInheritanceTypeJoined(): bool
1552 {
1553 return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED;
1554 }
1555
1556 /**
1557 * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
1558 *
1559 * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
1560 * FALSE otherwise.
1561 */
1562 public function isInheritanceTypeSingleTable(): bool
1563 {
1564 return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE;
1565 }
1566
1567 /**
1568 * Checks whether the class uses an identity column for the Id generation.
1569 */
1570 public function isIdGeneratorIdentity(): bool
1571 {
1572 return $this->generatorType === self::GENERATOR_TYPE_IDENTITY;
1573 }
1574
1575 /**
1576 * Checks whether the class uses a sequence for id generation.
1577 *
1578 * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition
1579 */
1580 public function isIdGeneratorSequence(): bool
1581 {
1582 return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE;
1583 }
1584
1585 /**
1586 * Checks whether the class has a natural identifier/pk (which means it does
1587 * not use any Id generator.
1588 */
1589 public function isIdentifierNatural(): bool
1590 {
1591 return $this->generatorType === self::GENERATOR_TYPE_NONE;
1592 }
1593
1594 /**
1595 * Gets the type of a field.
1596 *
1597 * @todo 3.0 Remove this. PersisterHelper should fix it somehow
1598 */
1599 public function getTypeOfField(string $fieldName): string|null
1600 {
1601 return isset($this->fieldMappings[$fieldName])
1602 ? $this->fieldMappings[$fieldName]->type
1603 : null;
1604 }
1605
1606 /**
1607 * Gets the name of the primary table.
1608 */
1609 public function getTableName(): string
1610 {
1611 return $this->table['name'];
1612 }
1613
1614 /**
1615 * Gets primary table's schema name.
1616 */
1617 public function getSchemaName(): string|null
1618 {
1619 return $this->table['schema'] ?? null;
1620 }
1621
1622 /**
1623 * Gets the table name to use for temporary identifier tables of this class.
1624 */
1625 public function getTemporaryIdTableName(): string
1626 {
1627 // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
1628 return str_replace('.', '_', $this->getTableName() . '_id_tmp');
1629 }
1630
1631 /**
1632 * Sets the mapped subclasses of this class.
1633 *
1634 * @psalm-param list<string> $subclasses The names of all mapped subclasses.
1635 */
1636 public function setSubclasses(array $subclasses): void
1637 {
1638 foreach ($subclasses as $subclass) {
1639 $this->subClasses[] = $this->fullyQualifiedClassName($subclass);
1640 }
1641 }
1642
1643 /**
1644 * Sets the parent class names. Only <em>entity</em> classes may be given.
1645 *
1646 * Assumes that the class names in the passed array are in the order:
1647 * directParent -> directParentParent -> directParentParentParent ... -> root.
1648 *
1649 * @psalm-param list<class-string> $classNames
1650 */
1651 public function setParentClasses(array $classNames): void
1652 {
1653 $this->parentClasses = $classNames;
1654
1655 if (count($classNames) > 0) {
1656 $this->rootEntityName = array_pop($classNames);
1657 }
1658 }
1659
1660 /**
1661 * Sets the inheritance type used by the class and its subclasses.
1662 *
1663 * @psalm-param self::INHERITANCE_TYPE_* $type
1664 *
1665 * @throws MappingException
1666 */
1667 public function setInheritanceType(int $type): void
1668 {
1669 if (! $this->isInheritanceType($type)) {
1670 throw MappingException::invalidInheritanceType($this->name, $type);
1671 }
1672
1673 $this->inheritanceType = $type;
1674 }
1675
1676 /**
1677 * Sets the association to override association mapping of property for an entity relationship.
1678 *
1679 * @psalm-param array<string, mixed> $overrideMapping
1680 *
1681 * @throws MappingException
1682 */
1683 public function setAssociationOverride(string $fieldName, array $overrideMapping): void
1684 {
1685 if (! isset($this->associationMappings[$fieldName])) {
1686 throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
1687 }
1688
1689 $mapping = $this->associationMappings[$fieldName]->toArray();
1690
1691 if (isset($mapping['inherited'])) {
1692 throw MappingException::illegalOverrideOfInheritedProperty(
1693 $this->name,
1694 $fieldName,
1695 $mapping['inherited'],
1696 );
1697 }
1698
1699 if (isset($overrideMapping['joinColumns'])) {
1700 $mapping['joinColumns'] = $overrideMapping['joinColumns'];
1701 }
1702
1703 if (isset($overrideMapping['inversedBy'])) {
1704 $mapping['inversedBy'] = $overrideMapping['inversedBy'];
1705 }
1706
1707 if (isset($overrideMapping['joinTable'])) {
1708 $mapping['joinTable'] = $overrideMapping['joinTable'];
1709 }
1710
1711 if (isset($overrideMapping['fetch'])) {
1712 $mapping['fetch'] = $overrideMapping['fetch'];
1713 }
1714
1715 switch ($mapping['type']) {
1716 case self::ONE_TO_ONE:
1717 case self::MANY_TO_ONE:
1718 $mapping['joinColumnFieldNames'] = [];
1719 $mapping['sourceToTargetKeyColumns'] = [];
1720 break;
1721 case self::MANY_TO_MANY:
1722 $mapping['relationToSourceKeyColumns'] = [];
1723 $mapping['relationToTargetKeyColumns'] = [];
1724 break;
1725 }
1726
1727 $this->associationMappings[$fieldName] = $this->_validateAndCompleteAssociationMapping($mapping);
1728 }
1729
1730 /**
1731 * Sets the override for a mapped field.
1732 *
1733 * @psalm-param array<string, mixed> $overrideMapping
1734 *
1735 * @throws MappingException
1736 */
1737 public function setAttributeOverride(string $fieldName, array $overrideMapping): void
1738 {
1739 if (! isset($this->fieldMappings[$fieldName])) {
1740 throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
1741 }
1742
1743 $mapping = $this->fieldMappings[$fieldName];
1744
1745 if (isset($mapping->inherited)) {
1746 throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName, $mapping->inherited);
1747 }
1748
1749 if (isset($mapping->id)) {
1750 $overrideMapping['id'] = $mapping->id;
1751 }
1752
1753 if (isset($mapping->declared)) {
1754 $overrideMapping['declared'] = $mapping->declared;
1755 }
1756
1757 if (! isset($overrideMapping['type'])) {
1758 $overrideMapping['type'] = $mapping->type;
1759 }
1760
1761 if (! isset($overrideMapping['fieldName'])) {
1762 $overrideMapping['fieldName'] = $mapping->fieldName;
1763 }
1764
1765 if ($overrideMapping['type'] !== $mapping->type) {
1766 throw MappingException::invalidOverrideFieldType($this->name, $fieldName);
1767 }
1768
1769 unset($this->fieldMappings[$fieldName]);
1770 unset($this->fieldNames[$mapping->columnName]);
1771 unset($this->columnNames[$mapping->fieldName]);
1772
1773 $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
1774
1775 $this->fieldMappings[$fieldName] = $overrideMapping;
1776 }
1777
1778 /**
1779 * Checks whether a mapped field is inherited from an entity superclass.
1780 */
1781 public function isInheritedField(string $fieldName): bool
1782 {
1783 return isset($this->fieldMappings[$fieldName]->inherited);
1784 }
1785
1786 /**
1787 * Checks if this entity is the root in any entity-inheritance-hierarchy.
1788 */
1789 public function isRootEntity(): bool
1790 {
1791 return $this->name === $this->rootEntityName;
1792 }
1793
1794 /**
1795 * Checks whether a mapped association field is inherited from a superclass.
1796 */
1797 public function isInheritedAssociation(string $fieldName): bool
1798 {
1799 return isset($this->associationMappings[$fieldName]->inherited);
1800 }
1801
1802 public function isInheritedEmbeddedClass(string $fieldName): bool
1803 {
1804 return isset($this->embeddedClasses[$fieldName]->inherited);
1805 }
1806
1807 /**
1808 * Sets the name of the primary table the class is mapped to.
1809 *
1810 * @deprecated Use {@link setPrimaryTable}.
1811 */
1812 public function setTableName(string $tableName): void
1813 {
1814 $this->table['name'] = $tableName;
1815 }
1816
1817 /**
1818 * Sets the primary table definition. The provided array supports the
1819 * following structure:
1820 *
1821 * name => <tableName> (optional, defaults to class name)
1822 * indexes => array of indexes (optional)
1823 * uniqueConstraints => array of constraints (optional)
1824 *
1825 * If a key is omitted, the current value is kept.
1826 *
1827 * @psalm-param array<string, mixed> $table The table description.
1828 */
1829 public function setPrimaryTable(array $table): void
1830 {
1831 if (isset($table['name'])) {
1832 // Split schema and table name from a table name like "myschema.mytable"
1833 if (str_contains($table['name'], '.')) {
1834 [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2);
1835 }
1836
1837 if ($table['name'][0] === '`') {
1838 $table['name'] = trim($table['name'], '`');
1839 $this->table['quoted'] = true;
1840 }
1841
1842 $this->table['name'] = $table['name'];
1843 }
1844
1845 if (isset($table['quoted'])) {
1846 $this->table['quoted'] = $table['quoted'];
1847 }
1848
1849 if (isset($table['schema'])) {
1850 $this->table['schema'] = $table['schema'];
1851 }
1852
1853 if (isset($table['indexes'])) {
1854 $this->table['indexes'] = $table['indexes'];
1855 }
1856
1857 if (isset($table['uniqueConstraints'])) {
1858 $this->table['uniqueConstraints'] = $table['uniqueConstraints'];
1859 }
1860
1861 if (isset($table['options'])) {
1862 $this->table['options'] = $table['options'];
1863 }
1864 }
1865
1866 /**
1867 * Checks whether the given type identifies an inheritance type.
1868 */
1869 private function isInheritanceType(int $type): bool
1870 {
1871 return $type === self::INHERITANCE_TYPE_NONE ||
1872 $type === self::INHERITANCE_TYPE_SINGLE_TABLE ||
1873 $type === self::INHERITANCE_TYPE_JOINED;
1874 }
1875
1876 /**
1877 * Adds a mapped field to the class.
1878 *
1879 * @psalm-param array<string, mixed> $mapping The field mapping.
1880 *
1881 * @throws MappingException
1882 */
1883 public function mapField(array $mapping): void
1884 {
1885 $mapping = $this->validateAndCompleteFieldMapping($mapping);
1886 $this->assertFieldNotMapped($mapping->fieldName);
1887
1888 if (isset($mapping->generated)) {
1889 $this->requiresFetchAfterChange = true;
1890 }
1891
1892 $this->fieldMappings[$mapping->fieldName] = $mapping;
1893 }
1894
1895 /**
1896 * INTERNAL:
1897 * Adds an association mapping without completing/validating it.
1898 * This is mainly used to add inherited association mappings to derived classes.
1899 *
1900 * @param ConcreteAssociationMapping $mapping
1901 *
1902 * @throws MappingException
1903 */
1904 public function addInheritedAssociationMapping(AssociationMapping $mapping/*, $owningClassName = null*/): void
1905 {
1906 if (isset($this->associationMappings[$mapping->fieldName])) {
1907 throw MappingException::duplicateAssociationMapping($this->name, $mapping->fieldName);
1908 }
1909
1910 $this->associationMappings[$mapping->fieldName] = $mapping;
1911 }
1912
1913 /**
1914 * INTERNAL:
1915 * Adds a field mapping without completing/validating it.
1916 * This is mainly used to add inherited field mappings to derived classes.
1917 */
1918 public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
1919 {
1920 $this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping;
1921 $this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
1922 $this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
1923
1924 if (isset($fieldMapping->generated)) {
1925 $this->requiresFetchAfterChange = true;
1926 }
1927 }
1928
1929 /**
1930 * Adds a one-to-one mapping.
1931 *
1932 * @param array<string, mixed> $mapping The mapping.
1933 */
1934 public function mapOneToOne(array $mapping): void
1935 {
1936 $mapping['type'] = self::ONE_TO_ONE;
1937
1938 $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
1939
1940 $this->_storeAssociationMapping($mapping);
1941 }
1942
1943 /**
1944 * Adds a one-to-many mapping.
1945 *
1946 * @psalm-param array<string, mixed> $mapping The mapping.
1947 */
1948 public function mapOneToMany(array $mapping): void
1949 {
1950 $mapping['type'] = self::ONE_TO_MANY;
1951
1952 $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
1953
1954 $this->_storeAssociationMapping($mapping);
1955 }
1956
1957 /**
1958 * Adds a many-to-one mapping.
1959 *
1960 * @psalm-param array<string, mixed> $mapping The mapping.
1961 */
1962 public function mapManyToOne(array $mapping): void
1963 {
1964 $mapping['type'] = self::MANY_TO_ONE;
1965
1966 $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
1967
1968 $this->_storeAssociationMapping($mapping);
1969 }
1970
1971 /**
1972 * Adds a many-to-many mapping.
1973 *
1974 * @psalm-param array<string, mixed> $mapping The mapping.
1975 */
1976 public function mapManyToMany(array $mapping): void
1977 {
1978 $mapping['type'] = self::MANY_TO_MANY;
1979
1980 $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
1981
1982 $this->_storeAssociationMapping($mapping);
1983 }
1984
1985 /**
1986 * Stores the association mapping.
1987 *
1988 * @param ConcreteAssociationMapping $assocMapping
1989 *
1990 * @throws MappingException
1991 */
1992 protected function _storeAssociationMapping(AssociationMapping $assocMapping): void
1993 {
1994 $sourceFieldName = $assocMapping->fieldName;
1995
1996 $this->assertFieldNotMapped($sourceFieldName);
1997
1998 $this->associationMappings[$sourceFieldName] = $assocMapping;
1999 }
2000
2001 /**
2002 * Registers a custom repository class for the entity class.
2003 *
2004 * @param string|null $repositoryClassName The class name of the custom mapper.
2005 * @psalm-param class-string<EntityRepository>|null $repositoryClassName
2006 */
2007 public function setCustomRepositoryClass(string|null $repositoryClassName): void
2008 {
2009 if ($repositoryClassName === null) {
2010 $this->customRepositoryClassName = null;
2011
2012 return;
2013 }
2014
2015 $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
2016 }
2017
2018 /**
2019 * Dispatches the lifecycle event of the given entity to the registered
2020 * lifecycle callbacks and lifecycle listeners.
2021 *
2022 * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
2023 *
2024 * @param string $lifecycleEvent The lifecycle event.
2025 */
2026 public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void
2027 {
2028 foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) {
2029 $entity->$callback();
2030 }
2031 }
2032
2033 /**
2034 * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
2035 */
2036 public function hasLifecycleCallbacks(string $lifecycleEvent): bool
2037 {
2038 return isset($this->lifecycleCallbacks[$lifecycleEvent]);
2039 }
2040
2041 /**
2042 * Gets the registered lifecycle callbacks for an event.
2043 *
2044 * @return string[]
2045 * @psalm-return list<string>
2046 */
2047 public function getLifecycleCallbacks(string $event): array
2048 {
2049 return $this->lifecycleCallbacks[$event] ?? [];
2050 }
2051
2052 /**
2053 * Adds a lifecycle callback for entities of this class.
2054 */
2055 public function addLifecycleCallback(string $callback, string $event): void
2056 {
2057 if ($this->isEmbeddedClass) {
2058 throw MappingException::illegalLifecycleCallbackOnEmbeddedClass($callback, $this->name);
2059 }
2060
2061 if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
2062 return;
2063 }
2064
2065 $this->lifecycleCallbacks[$event][] = $callback;
2066 }
2067
2068 /**
2069 * Sets the lifecycle callbacks for entities of this class.
2070 * Any previously registered callbacks are overwritten.
2071 *
2072 * @psalm-param array<string, list<string>> $callbacks
2073 */
2074 public function setLifecycleCallbacks(array $callbacks): void
2075 {
2076 $this->lifecycleCallbacks = $callbacks;
2077 }
2078
2079 /**
2080 * Adds a entity listener for entities of this class.
2081 *
2082 * @param string $eventName The entity lifecycle event.
2083 * @param string $class The listener class.
2084 * @param string $method The listener callback method.
2085 *
2086 * @throws MappingException
2087 */
2088 public function addEntityListener(string $eventName, string $class, string $method): void
2089 {
2090 $class = $this->fullyQualifiedClassName($class);
2091
2092 $listener = [
2093 'class' => $class,
2094 'method' => $method,
2095 ];
2096
2097 if (! class_exists($class)) {
2098 throw MappingException::entityListenerClassNotFound($class, $this->name);
2099 }
2100
2101 if (! method_exists($class, $method)) {
2102 throw MappingException::entityListenerMethodNotFound($class, $method, $this->name);
2103 }
2104
2105 if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
2106 throw MappingException::duplicateEntityListener($class, $method, $this->name);
2107 }
2108
2109 $this->entityListeners[$eventName][] = $listener;
2110 }
2111
2112 /**
2113 * Sets the discriminator column definition.
2114 *
2115 * @see getDiscriminatorColumn()
2116 *
2117 * @param DiscriminatorColumnMapping|mixed[]|null $columnDef
2118 * @psalm-param DiscriminatorColumnMapping|array{
2119 * name: string|null,
2120 * fieldName?: string|null,
2121 * type?: string|null,
2122 * length?: int|null,
2123 * columnDefinition?: string|null,
2124 * enumType?: class-string<BackedEnum>|null,
2125 * options?: array<string, mixed>|null
2126 * }|null $columnDef
2127 *
2128 * @throws MappingException
2129 */
2130 public function setDiscriminatorColumn(DiscriminatorColumnMapping|array|null $columnDef): void
2131 {
2132 if ($columnDef instanceof DiscriminatorColumnMapping) {
2133 $this->discriminatorColumn = $columnDef;
2134
2135 return;
2136 }
2137
2138 if ($columnDef !== null) {
2139 if (! isset($columnDef['name'])) {
2140 throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
2141 }
2142
2143 if (isset($this->fieldNames[$columnDef['name']])) {
2144 throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
2145 }
2146
2147 $columnDef['fieldName'] ??= $columnDef['name'];
2148 $columnDef['type'] ??= 'string';
2149 $columnDef['options'] ??= [];
2150
2151 if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
2152 throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
2153 }
2154
2155 $this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef);
2156 }
2157 }
2158
2159 final public function getDiscriminatorColumn(): DiscriminatorColumnMapping
2160 {
2161 if ($this->discriminatorColumn === null) {
2162 throw new LogicException('The discriminator column was not set.');
2163 }
2164
2165 return $this->discriminatorColumn;
2166 }
2167
2168 /**
2169 * Sets the discriminator values used by this class.
2170 * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
2171 *
2172 * @param array<int|string, string> $map
2173 */
2174 public function setDiscriminatorMap(array $map): void
2175 {
2176 foreach ($map as $value => $className) {
2177 $this->addDiscriminatorMapClass($value, $className);
2178 }
2179 }
2180
2181 /**
2182 * Adds one entry of the discriminator map with a new class and corresponding name.
2183 *
2184 * @throws MappingException
2185 */
2186 public function addDiscriminatorMapClass(int|string $name, string $className): void
2187 {
2188 $className = $this->fullyQualifiedClassName($className);
2189 $className = ltrim($className, '\\');
2190
2191 $this->discriminatorMap[$name] = $className;
2192
2193 if ($this->name === $className) {
2194 $this->discriminatorValue = $name;
2195
2196 return;
2197 }
2198
2199 if (! (class_exists($className) || interface_exists($className))) {
2200 throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
2201 }
2202
2203 $this->addSubClass($className);
2204 }
2205
2206 /** @param array<class-string> $classes */
2207 public function addSubClasses(array $classes): void
2208 {
2209 foreach ($classes as $className) {
2210 $this->addSubClass($className);
2211 }
2212 }
2213
2214 public function addSubClass(string $className): void
2215 {
2216 // By ignoring classes that are not subclasses of the current class, we simplify inheriting
2217 // the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata.
2218
2219 if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
2220 $this->subClasses[] = $className;
2221 }
2222 }
2223
2224 public function hasAssociation(string $fieldName): bool
2225 {
2226 return isset($this->associationMappings[$fieldName]);
2227 }
2228
2229 public function isSingleValuedAssociation(string $fieldName): bool
2230 {
2231 return isset($this->associationMappings[$fieldName])
2232 && ($this->associationMappings[$fieldName]->isToOne());
2233 }
2234
2235 public function isCollectionValuedAssociation(string $fieldName): bool
2236 {
2237 return isset($this->associationMappings[$fieldName])
2238 && ! $this->associationMappings[$fieldName]->isToOne();
2239 }
2240
2241 /**
2242 * Is this an association that only has a single join column?
2243 */
2244 public function isAssociationWithSingleJoinColumn(string $fieldName): bool
2245 {
2246 return isset($this->associationMappings[$fieldName])
2247 && isset($this->associationMappings[$fieldName]->joinColumns[0])
2248 && ! isset($this->associationMappings[$fieldName]->joinColumns[1]);
2249 }
2250
2251 /**
2252 * Returns the single association join column (if any).
2253 *
2254 * @throws MappingException
2255 */
2256 public function getSingleAssociationJoinColumnName(string $fieldName): string
2257 {
2258 if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
2259 throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
2260 }
2261
2262 $assoc = $this->associationMappings[$fieldName];
2263
2264 assert($assoc->isToOneOwningSide());
2265
2266 return $assoc->joinColumns[0]->name;
2267 }
2268
2269 /**
2270 * Returns the single association referenced join column name (if any).
2271 *
2272 * @throws MappingException
2273 */
2274 public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string
2275 {
2276 if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
2277 throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
2278 }
2279
2280 $assoc = $this->associationMappings[$fieldName];
2281
2282 assert($assoc->isToOneOwningSide());
2283
2284 return $assoc->joinColumns[0]->referencedColumnName;
2285 }
2286
2287 /**
2288 * Used to retrieve a fieldname for either field or association from a given column.
2289 *
2290 * This method is used in foreign-key as primary-key contexts.
2291 *
2292 * @throws MappingException
2293 */
2294 public function getFieldForColumn(string $columnName): string
2295 {
2296 if (isset($this->fieldNames[$columnName])) {
2297 return $this->fieldNames[$columnName];
2298 }
2299
2300 foreach ($this->associationMappings as $assocName => $mapping) {
2301 if (
2302 $this->isAssociationWithSingleJoinColumn($assocName) &&
2303 assert($this->associationMappings[$assocName]->isToOneOwningSide()) &&
2304 $this->associationMappings[$assocName]->joinColumns[0]->name === $columnName
2305 ) {
2306 return $assocName;
2307 }
2308 }
2309
2310 throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
2311 }
2312
2313 /**
2314 * Sets the ID generator used to generate IDs for instances of this class.
2315 */
2316 public function setIdGenerator(AbstractIdGenerator $generator): void
2317 {
2318 $this->idGenerator = $generator;
2319 }
2320
2321 /**
2322 * Sets definition.
2323 *
2324 * @psalm-param array<string, string|null> $definition
2325 */
2326 public function setCustomGeneratorDefinition(array $definition): void
2327 {
2328 $this->customGeneratorDefinition = $definition;
2329 }
2330
2331 /**
2332 * Sets the definition of the sequence ID generator for this class.
2333 *
2334 * The definition must have the following structure:
2335 * <code>
2336 * array(
2337 * 'sequenceName' => 'name',
2338 * 'allocationSize' => 20,
2339 * 'initialValue' => 1
2340 * 'quoted' => 1
2341 * )
2342 * </code>
2343 *
2344 * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
2345 *
2346 * @throws MappingException
2347 */
2348 public function setSequenceGeneratorDefinition(array $definition): void
2349 {
2350 if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
2351 throw MappingException::missingSequenceName($this->name);
2352 }
2353
2354 if ($definition['sequenceName'][0] === '`') {
2355 $definition['sequenceName'] = trim($definition['sequenceName'], '`');
2356 $definition['quoted'] = true;
2357 }
2358
2359 if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') {
2360 $definition['allocationSize'] = '1';
2361 }
2362
2363 if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') {
2364 $definition['initialValue'] = '1';
2365 }
2366
2367 $definition['allocationSize'] = (string) $definition['allocationSize'];
2368 $definition['initialValue'] = (string) $definition['initialValue'];
2369
2370 $this->sequenceGeneratorDefinition = $definition;
2371 }
2372
2373 /**
2374 * Sets the version field mapping used for versioning. Sets the default
2375 * value to use depending on the column type.
2376 *
2377 * @psalm-param array<string, mixed> $mapping The version field mapping array.
2378 *
2379 * @throws MappingException
2380 */
2381 public function setVersionMapping(array &$mapping): void
2382 {
2383 $this->isVersioned = true;
2384 $this->versionField = $mapping['fieldName'];
2385 $this->requiresFetchAfterChange = true;
2386
2387 if (! isset($mapping['default'])) {
2388 if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
2389 $mapping['default'] = 1;
2390 } elseif ($mapping['type'] === 'datetime') {
2391 $mapping['default'] = 'CURRENT_TIMESTAMP';
2392 } else {
2393 throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
2394 }
2395 }
2396 }
2397
2398 /**
2399 * Sets whether this class is to be versioned for optimistic locking.
2400 */
2401 public function setVersioned(bool $bool): void
2402 {
2403 $this->isVersioned = $bool;
2404
2405 if ($bool) {
2406 $this->requiresFetchAfterChange = true;
2407 }
2408 }
2409
2410 /**
2411 * Sets the name of the field that is to be used for versioning if this class is
2412 * versioned for optimistic locking.
2413 */
2414 public function setVersionField(string|null $versionField): void
2415 {
2416 $this->versionField = $versionField;
2417 }
2418
2419 /**
2420 * Marks this class as read only, no change tracking is applied to it.
2421 */
2422 public function markReadOnly(): void
2423 {
2424 $this->isReadOnly = true;
2425 }
2426
2427 /**
2428 * {@inheritDoc}
2429 */
2430 public function getFieldNames(): array
2431 {
2432 return array_keys($this->fieldMappings);
2433 }
2434
2435 /**
2436 * {@inheritDoc}
2437 */
2438 public function getAssociationNames(): array
2439 {
2440 return array_keys($this->associationMappings);
2441 }
2442
2443 /**
2444 * {@inheritDoc}
2445 *
2446 * @psalm-return class-string
2447 *
2448 * @throws InvalidArgumentException
2449 */
2450 public function getAssociationTargetClass(string $assocName): string
2451 {
2452 return $this->associationMappings[$assocName]->targetEntity
2453 ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
2454 }
2455
2456 public function getName(): string
2457 {
2458 return $this->name;
2459 }
2460
2461 public function isAssociationInverseSide(string $assocName): bool
2462 {
2463 return isset($this->associationMappings[$assocName])
2464 && ! $this->associationMappings[$assocName]->isOwningSide();
2465 }
2466
2467 public function getAssociationMappedByTargetField(string $assocName): string
2468 {
2469 $assoc = $this->getAssociationMapping($assocName);
2470
2471 if (! $assoc instanceof InverseSideMapping) {
2472 throw new LogicException(sprintf(
2473 <<<'EXCEPTION'
2474 Context: Calling %s() with "%s", which is the owning side of an association.
2475 Problem: The owning side of an association has no "mappedBy" field.
2476 Solution: Call %s::isAssociationInverseSide() to check first.
2477 EXCEPTION,
2478 __METHOD__,
2479 $assocName,
2480 self::class,
2481 ));
2482 }
2483
2484 return $assoc->mappedBy;
2485 }
2486
2487 /**
2488 * @param C $className
2489 *
2490 * @return string|null null if and only if the input value is null
2491 * @psalm-return (C is class-string ? class-string : (C is string ? string : null))
2492 *
2493 * @template C of string|null
2494 */
2495 public function fullyQualifiedClassName(string|null $className): string|null
2496 {
2497 if ($className === null) {
2498 Deprecation::trigger(
2499 'doctrine/orm',
2500 'https://github.com/doctrine/orm/pull/11294',
2501 'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0',
2502 __METHOD__,
2503 );
2504
2505 return null;
2506 }
2507
2508 if (! str_contains($className, '\\') && $this->namespace) {
2509 return $this->namespace . '\\' . $className;
2510 }
2511
2512 return $className;
2513 }
2514
2515 public function getMetadataValue(string $name): mixed
2516 {
2517 return $this->$name ?? null;
2518 }
2519
2520 /**
2521 * Map Embedded Class
2522 *
2523 * @psalm-param array{
2524 * fieldName: string,
2525 * class?: class-string,
2526 * declaredField?: string,
2527 * columnPrefix?: string|false|null,
2528 * originalField?: string
2529 * } $mapping
2530 *
2531 * @throws MappingException
2532 */
2533 public function mapEmbedded(array $mapping): void
2534 {
2535 $this->assertFieldNotMapped($mapping['fieldName']);
2536
2537 if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
2538 $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
2539 if ($type instanceof ReflectionNamedType) {
2540 $mapping['class'] = $type->getName();
2541 }
2542 }
2543
2544 if (! (isset($mapping['class']) && $mapping['class'])) {
2545 throw MappingException::missingEmbeddedClass($mapping['fieldName']);
2546 }
2547
2548 $this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([
2549 'class' => $this->fullyQualifiedClassName($mapping['class']),
2550 'columnPrefix' => $mapping['columnPrefix'] ?? null,
2551 'declaredField' => $mapping['declaredField'] ?? null,
2552 'originalField' => $mapping['originalField'] ?? null,
2553 ]);
2554 }
2555
2556 /**
2557 * Inline the embeddable class
2558 */
2559 public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void
2560 {
2561 foreach ($embeddable->fieldMappings as $originalFieldMapping) {
2562 $fieldMapping = (array) $originalFieldMapping;
2563 $fieldMapping['originalClass'] ??= $embeddable->name;
2564 $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
2565 ? $property . '.' . $fieldMapping['declaredField']
2566 : $property;
2567 $fieldMapping['originalField'] ??= $fieldMapping['fieldName'];
2568 $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName'];
2569
2570 if (! empty($this->embeddedClasses[$property]->columnPrefix)) {
2571 $fieldMapping['columnName'] = $this->embeddedClasses[$property]->columnPrefix . $fieldMapping['columnName'];
2572 } elseif ($this->embeddedClasses[$property]->columnPrefix !== false) {
2573 assert($this->reflClass !== null);
2574 assert($embeddable->reflClass !== null);
2575 $fieldMapping['columnName'] = $this->namingStrategy
2576 ->embeddedFieldToColumnName(
2577 $property,
2578 $fieldMapping['columnName'],
2579 $this->reflClass->name,
2580 $embeddable->reflClass->name,
2581 );
2582 }
2583
2584 $this->mapField($fieldMapping);
2585 }
2586 }
2587
2588 /** @throws MappingException */
2589 private function assertFieldNotMapped(string $fieldName): void
2590 {
2591 if (
2592 isset($this->fieldMappings[$fieldName]) ||
2593 isset($this->associationMappings[$fieldName]) ||
2594 isset($this->embeddedClasses[$fieldName])
2595 ) {
2596 throw MappingException::duplicateFieldMapping($this->name, $fieldName);
2597 }
2598 }
2599
2600 /**
2601 * Gets the sequence name based on class metadata.
2602 *
2603 * @todo Sequence names should be computed in DBAL depending on the platform
2604 */
2605 public function getSequenceName(AbstractPlatform $platform): string
2606 {
2607 $sequencePrefix = $this->getSequencePrefix($platform);
2608 $columnName = $this->getSingleIdentifierColumnName();
2609
2610 return $sequencePrefix . '_' . $columnName . '_seq';
2611 }
2612
2613 /**
2614 * Gets the sequence name prefix based on class metadata.
2615 *
2616 * @todo Sequence names should be computed in DBAL depending on the platform
2617 */
2618 public function getSequencePrefix(AbstractPlatform $platform): string
2619 {
2620 $tableName = $this->getTableName();
2621 $sequencePrefix = $tableName;
2622
2623 // Prepend the schema name to the table name if there is one
2624 $schemaName = $this->getSchemaName();
2625 if ($schemaName) {
2626 $sequencePrefix = $schemaName . '.' . $tableName;
2627 }
2628
2629 return $sequencePrefix;
2630 }
2631
2632 /** @psalm-param class-string $class */
2633 private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
2634 {
2635 $reflectionProperty = $reflService->getAccessibleProperty($class, $field);
2636 if ($reflectionProperty?->isReadOnly()) {
2637 $declaringClass = $reflectionProperty->class;
2638 if ($declaringClass !== $class) {
2639 $reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field);
2640 }
2641
2642 if ($reflectionProperty !== null) {
2643 $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
2644 }
2645 }
2646
2647 return $reflectionProperty;
2648 }
2649}