summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Mapping
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/Mapping')
-rw-r--r--vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php76
-rw-r--r--vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php70
-rw-r--r--vendor/doctrine/orm/src/Mapping/AssociationMapping.php359
-rw-r--r--vendor/doctrine/orm/src/Mapping/AssociationOverride.php51
-rw-r--r--vendor/doctrine/orm/src/Mapping/AssociationOverrides.php38
-rw-r--r--vendor/doctrine/orm/src/Mapping/AttributeOverride.php15
-rw-r--r--vendor/doctrine/orm/src/Mapping/AttributeOverrides.php38
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php171
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php426
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php46
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php55
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php243
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php73
-rw-r--r--vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php46
-rw-r--r--vendor/doctrine/orm/src/Mapping/Cache.php19
-rw-r--r--vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php35
-rw-r--r--vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/ClassMetadata.php2649
-rw-r--r--vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php729
-rw-r--r--vendor/doctrine/orm/src/Mapping/Column.php36
-rw-r--r--vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php16
-rw-r--r--vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php40
-rw-r--r--vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php68
-rw-r--r--vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php145
-rw-r--r--vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php80
-rw-r--r--vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php24
-rw-r--r--vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php83
-rw-r--r--vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php768
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php146
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php528
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php44
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php16
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php25
-rw-r--r--vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php940
-rw-r--r--vendor/doctrine/orm/src/Mapping/Embeddable.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/Embedded.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php93
-rw-r--r--vendor/doctrine/orm/src/Mapping/Entity.php20
-rw-r--r--vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php30
-rw-r--r--vendor/doctrine/orm/src/Mapping/EntityListeners.php21
-rw-r--r--vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php28
-rw-r--r--vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php16
-rw-r--r--vendor/doctrine/orm/src/Mapping/FieldMapping.php169
-rw-r--r--vendor/doctrine/orm/src/Mapping/GeneratedValue.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/Id.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/Index.php26
-rw-r--r--vendor/doctrine/orm/src/Mapping/InheritanceType.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php13
-rw-r--r--vendor/doctrine/orm/src/Mapping/InverseSideMapping.php30
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinColumn.php13
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php77
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php21
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinColumns.php14
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinTable.php35
-rw-r--r--vendor/doctrine/orm/src/Mapping/JoinTableMapping.php115
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToMany.php27
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php185
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToOne.php24
-rw-r--r--vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/MappedSuperclass.php18
-rw-r--r--vendor/doctrine/orm/src/Mapping/MappingAttribute.php10
-rw-r--r--vendor/doctrine/orm/src/Mapping/MappingException.php691
-rw-r--r--vendor/doctrine/orm/src/Mapping/NamingStrategy.php71
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToMany.php26
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php75
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToOne.php26
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/OrderBy.php17
-rw-r--r--vendor/doctrine/orm/src/Mapping/OwningSideMapping.php28
-rw-r--r--vendor/doctrine/orm/src/Mapping/PostLoad.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PostPersist.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PostRemove.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PostUpdate.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PreFlush.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PrePersist.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PreRemove.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/PreUpdate.php12
-rw-r--r--vendor/doctrine/orm/src/Mapping/QuoteStrategy.php68
-rw-r--r--vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php61
-rw-r--r--vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php87
-rw-r--r--vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php49
-rw-r--r--vendor/doctrine/orm/src/Mapping/SequenceGenerator.php18
-rw-r--r--vendor/doctrine/orm/src/Mapping/Table.php45
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php16
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php69
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php10
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php10
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php9
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php52
-rw-r--r--vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php212
-rw-r--r--vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php20
-rw-r--r--vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php108
-rw-r--r--vendor/doctrine/orm/src/Mapping/UniqueConstraint.php24
-rw-r--r--vendor/doctrine/orm/src/Mapping/Version.php12
100 files changed, 11161 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php
new file mode 100644
index 0000000..872d4d6
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php
@@ -0,0 +1,76 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\ORM\Internal\SQLResultCasing;
9
10/**
11 * ANSI compliant quote strategy, this strategy does not apply any quote.
12 * To use this strategy all mapped tables and columns should be ANSI compliant.
13 */
14class AnsiQuoteStrategy implements QuoteStrategy
15{
16 use SQLResultCasing;
17
18 public function getColumnName(
19 string $fieldName,
20 ClassMetadata $class,
21 AbstractPlatform $platform,
22 ): string {
23 return $class->fieldMappings[$fieldName]->columnName;
24 }
25
26 public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
27 {
28 return $class->table['name'];
29 }
30
31 /**
32 * {@inheritDoc}
33 */
34 public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
35 {
36 return $definition['sequenceName'];
37 }
38
39 public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
40 {
41 return $joinColumn->name;
42 }
43
44 public function getReferencedJoinColumnName(
45 JoinColumnMapping $joinColumn,
46 ClassMetadata $class,
47 AbstractPlatform $platform,
48 ): string {
49 return $joinColumn->referencedColumnName;
50 }
51
52 public function getJoinTableName(
53 ManyToManyOwningSideMapping $association,
54 ClassMetadata $class,
55 AbstractPlatform $platform,
56 ): string {
57 return $association->joinTable->name;
58 }
59
60 /**
61 * {@inheritDoc}
62 */
63 public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
64 {
65 return $class->identifier;
66 }
67
68 public function getColumnAlias(
69 string $columnName,
70 int $counter,
71 AbstractPlatform $platform,
72 ClassMetadata|null $class = null,
73 ): string {
74 return $this->getSQLResultCasing($platform, $columnName . '_' . $counter);
75 }
76}
diff --git a/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php b/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php
new file mode 100644
index 0000000..3fd0988
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php
@@ -0,0 +1,70 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\Deprecations\Deprecation;
8use InvalidArgumentException;
9
10use function property_exists;
11
12/** @internal */
13trait ArrayAccessImplementation
14{
15 /** @param string $offset */
16 public function offsetExists(mixed $offset): bool
17 {
18 Deprecation::trigger(
19 'doctrine/orm',
20 'https://github.com/doctrine/orm/pull/11211',
21 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
22 static::class,
23 );
24
25 return isset($this->$offset);
26 }
27
28 /** @param string $offset */
29 public function offsetGet(mixed $offset): mixed
30 {
31 Deprecation::trigger(
32 'doctrine/orm',
33 'https://github.com/doctrine/orm/pull/11211',
34 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
35 static::class,
36 );
37
38 if (! property_exists($this, $offset)) {
39 throw new InvalidArgumentException('Undefined property: ' . $offset);
40 }
41
42 return $this->$offset;
43 }
44
45 /** @param string $offset */
46 public function offsetSet(mixed $offset, mixed $value): void
47 {
48 Deprecation::trigger(
49 'doctrine/orm',
50 'https://github.com/doctrine/orm/pull/11211',
51 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
52 static::class,
53 );
54
55 $this->$offset = $value;
56 }
57
58 /** @param string $offset */
59 public function offsetUnset(mixed $offset): void
60 {
61 Deprecation::trigger(
62 'doctrine/orm',
63 'https://github.com/doctrine/orm/pull/11211',
64 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
65 static::class,
66 );
67
68 $this->$offset = null;
69 }
70}
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationMapping.php b/vendor/doctrine/orm/src/Mapping/AssociationMapping.php
new file mode 100644
index 0000000..ce7bdb4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AssociationMapping.php
@@ -0,0 +1,359 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8use Exception;
9use OutOfRangeException;
10
11use function assert;
12use function count;
13use function in_array;
14use function property_exists;
15use function sprintf;
16
17/** @template-implements ArrayAccess<string, mixed> */
18abstract class AssociationMapping implements ArrayAccess
19{
20 /**
21 * The names of persistence operations to cascade on the association.
22 *
23 * @var list<'persist'|'remove'|'detach'|'refresh'|'all'>
24 */
25 public array $cascade = [];
26
27 /**
28 * The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
29 *
30 * @var ClassMetadata::FETCH_*|null
31 */
32 public int|null $fetch = null;
33
34 /**
35 * This is set when the association is inherited by this class from another
36 * (inheritance) parent <em>entity</em> class. The value is the FQCN of the
37 * topmost entity class that contains this association. (If there are
38 * transient classes in the class hierarchy, these are ignored, so the
39 * class property may in fact come from a class further up in the PHP class
40 * hierarchy.) To-many associations initially declared in mapped
41 * superclasses are <em>not</em> considered 'inherited' in the nearest
42 * entity subclasses.
43 *
44 * @var class-string|null
45 */
46 public string|null $inherited = null;
47
48 /**
49 * This is set when the association does not appear in the current class
50 * for the first time, but is initially declared in another parent
51 * <em>entity or mapped superclass</em>. The value is the FQCN of the
52 * topmost non-transient class that contains association information for
53 * this relationship.
54 *
55 * @var class-string|null
56 */
57 public string|null $declared = null;
58
59 public array|null $cache = null;
60
61 public bool|null $id = null;
62
63 public bool|null $isOnDeleteCascade = null;
64
65 /** @var class-string|null */
66 public string|null $originalClass = null;
67
68 public string|null $originalField = null;
69
70 public bool $orphanRemoval = false;
71
72 public bool|null $unique = null;
73
74 /**
75 * @param string $fieldName The name of the field in the entity
76 * the association is mapped to.
77 * @param class-string $sourceEntity The class name of the source entity.
78 * In the case of to-many associations
79 * initially present in mapped
80 * superclasses, the nearest
81 * <em>entity</em> subclasses will be
82 * considered the respective source
83 * entities.
84 * @param class-string $targetEntity The class name of the target entity.
85 * If it is fully-qualified it is used as
86 * is. If it is a simple, unqualified
87 * class name the namespace is assumed to
88 * be the same as the namespace of the
89 * source entity.
90 */
91 final public function __construct(
92 public readonly string $fieldName,
93 public string $sourceEntity,
94 public readonly string $targetEntity,
95 ) {
96 }
97
98 /**
99 * @param mixed[] $mappingArray
100 * @psalm-param array{
101 * fieldName: string,
102 * sourceEntity: class-string,
103 * targetEntity: class-string,
104 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
105 * fetch?: ClassMetadata::FETCH_*|null,
106 * inherited?: class-string|null,
107 * declared?: class-string|null,
108 * cache?: array<mixed>|null,
109 * id?: bool|null,
110 * isOnDeleteCascade?: bool|null,
111 * originalClass?: class-string|null,
112 * originalField?: string|null,
113 * orphanRemoval?: bool,
114 * unique?: bool|null,
115 * joinTable?: mixed[]|null,
116 * type?: int,
117 * isOwningSide: bool,
118 * } $mappingArray
119 */
120 public static function fromMappingArray(array $mappingArray): static
121 {
122 unset($mappingArray['isOwningSide'], $mappingArray['type']);
123 $mapping = new static(
124 $mappingArray['fieldName'],
125 $mappingArray['sourceEntity'],
126 $mappingArray['targetEntity'],
127 );
128 unset($mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity']);
129
130 foreach ($mappingArray as $key => $value) {
131 if ($key === 'joinTable') {
132 assert($mapping instanceof ManyToManyAssociationMapping);
133
134 if ($value === [] || $value === null) {
135 continue;
136 }
137
138 assert($mapping instanceof ManyToManyOwningSideMapping);
139
140 $mapping->joinTable = JoinTableMapping::fromMappingArray($value);
141
142 continue;
143 }
144
145 if (property_exists($mapping, $key)) {
146 $mapping->$key = $value;
147 } else {
148 throw new OutOfRangeException('Unknown property ' . $key . ' on class ' . static::class);
149 }
150 }
151
152 return $mapping;
153 }
154
155 /**
156 * @psalm-assert-if-true OwningSideMapping $this
157 * @psalm-assert-if-false InverseSideMapping $this
158 */
159 final public function isOwningSide(): bool
160 {
161 return $this instanceof OwningSideMapping;
162 }
163
164 /** @psalm-assert-if-true ToOneAssociationMapping $this */
165 final public function isToOne(): bool
166 {
167 return $this instanceof ToOneAssociationMapping;
168 }
169
170 /** @psalm-assert-if-true ToManyAssociationMapping $this */
171 final public function isToMany(): bool
172 {
173 return $this instanceof ToManyAssociationMapping;
174 }
175
176 /** @psalm-assert-if-true OneToOneOwningSideMapping $this */
177 final public function isOneToOneOwningSide(): bool
178 {
179 return $this->isOneToOne() && $this->isOwningSide();
180 }
181
182 /** @psalm-assert-if-true OneToOneOwningSideMapping|ManyToOneAssociationMapping $this */
183 final public function isToOneOwningSide(): bool
184 {
185 return $this->isToOne() && $this->isOwningSide();
186 }
187
188 /** @psalm-assert-if-true ManyToManyOwningSideMapping $this */
189 final public function isManyToManyOwningSide(): bool
190 {
191 return $this instanceof ManyToManyOwningSideMapping;
192 }
193
194 /** @psalm-assert-if-true OneToOneAssociationMapping $this */
195 final public function isOneToOne(): bool
196 {
197 return $this instanceof OneToOneAssociationMapping;
198 }
199
200 /** @psalm-assert-if-true OneToManyAssociationMapping $this */
201 final public function isOneToMany(): bool
202 {
203 return $this instanceof OneToManyAssociationMapping;
204 }
205
206 /** @psalm-assert-if-true ManyToOneAssociationMapping $this */
207 final public function isManyToOne(): bool
208 {
209 return $this instanceof ManyToOneAssociationMapping;
210 }
211
212 /** @psalm-assert-if-true ManyToManyAssociationMapping $this */
213 final public function isManyToMany(): bool
214 {
215 return $this instanceof ManyToManyAssociationMapping;
216 }
217
218 /** @psalm-assert-if-true ToManyAssociationMapping $this */
219 final public function isOrdered(): bool
220 {
221 return $this->isToMany() && $this->orderBy() !== [];
222 }
223
224 /** @psalm-assert-if-true ToManyAssociationMapping $this */
225 public function isIndexed(): bool
226 {
227 return false;
228 }
229
230 final public function type(): int
231 {
232 return match (true) {
233 $this instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE,
234 $this instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY,
235 $this instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE,
236 $this instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY,
237 default => throw new Exception('Cannot determine type for ' . static::class),
238 };
239 }
240
241 /** @param string $offset */
242 public function offsetExists(mixed $offset): bool
243 {
244 return isset($this->$offset) || in_array($offset, ['isOwningSide', 'type'], true);
245 }
246
247 final public function offsetGet(mixed $offset): mixed
248 {
249 return match ($offset) {
250 'isOwningSide' => $this->isOwningSide(),
251 'type' => $this->type(),
252 'isCascadeRemove' => $this->isCascadeRemove(),
253 'isCascadePersist' => $this->isCascadePersist(),
254 'isCascadeRefresh' => $this->isCascadeRefresh(),
255 'isCascadeDetach' => $this->isCascadeDetach(),
256 default => property_exists($this, $offset) ? $this->$offset : throw new OutOfRangeException(sprintf(
257 'Unknown property "%s" on class %s',
258 $offset,
259 static::class,
260 )),
261 };
262 }
263
264 public function offsetSet(mixed $offset, mixed $value): void
265 {
266 assert($offset !== null);
267 if (! property_exists($this, $offset)) {
268 throw new OutOfRangeException(sprintf(
269 'Unknown property "%s" on class %s',
270 $offset,
271 static::class,
272 ));
273 }
274
275 if ($offset === 'joinTable') {
276 $value = JoinTableMapping::fromMappingArray($value);
277 }
278
279 $this->$offset = $value;
280 }
281
282 /** @param string $offset */
283 public function offsetUnset(mixed $offset): void
284 {
285 if (! property_exists($this, $offset)) {
286 throw new OutOfRangeException(sprintf(
287 'Unknown property "%s" on class %s',
288 $offset,
289 static::class,
290 ));
291 }
292
293 $this->$offset = null;
294 }
295
296 final public function isCascadeRemove(): bool
297 {
298 return in_array('remove', $this->cascade, true);
299 }
300
301 final public function isCascadePersist(): bool
302 {
303 return in_array('persist', $this->cascade, true);
304 }
305
306 final public function isCascadeRefresh(): bool
307 {
308 return in_array('refresh', $this->cascade, true);
309 }
310
311 final public function isCascadeDetach(): bool
312 {
313 return in_array('detach', $this->cascade, true);
314 }
315
316 /** @return array<string, mixed> */
317 public function toArray(): array
318 {
319 $array = (array) $this;
320
321 $array['isOwningSide'] = $this->isOwningSide();
322 $array['type'] = $this->type();
323
324 return $array;
325 }
326
327 /** @return list<string> */
328 public function __sleep(): array
329 {
330 $serialized = ['fieldName', 'sourceEntity', 'targetEntity'];
331
332 if (count($this->cascade) > 0) {
333 $serialized[] = 'cascade';
334 }
335
336 foreach (
337 [
338 'fetch',
339 'inherited',
340 'declared',
341 'cache',
342 'originalClass',
343 'originalField',
344 ] as $stringOrArrayProperty
345 ) {
346 if ($this->$stringOrArrayProperty !== null) {
347 $serialized[] = $stringOrArrayProperty;
348 }
349 }
350
351 foreach (['id', 'orphanRemoval', 'isOnDeleteCascade', 'unique'] as $boolProperty) {
352 if ($this->$boolProperty) {
353 $serialized[] = $boolProperty;
354 }
355 }
356
357 return $serialized;
358 }
359}
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationOverride.php b/vendor/doctrine/orm/src/Mapping/AssociationOverride.php
new file mode 100644
index 0000000..e0ebc07
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AssociationOverride.php
@@ -0,0 +1,51 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/** This attribute is used to override association mapping of property for an entity relationship. */
8final class AssociationOverride implements MappingAttribute
9{
10 /**
11 * The join column that is being mapped to the persistent attribute.
12 *
13 * @var array<JoinColumn>|null
14 */
15 public readonly array|null $joinColumns;
16
17 /**
18 * The join column that is being mapped to the persistent attribute.
19 *
20 * @var array<JoinColumn>|null
21 */
22 public readonly array|null $inverseJoinColumns;
23
24 /**
25 * @param string $name The name of the relationship property whose mapping is being overridden.
26 * @param JoinColumn|array<JoinColumn> $joinColumns
27 * @param JoinColumn|array<JoinColumn> $inverseJoinColumns
28 * @param JoinTable|null $joinTable The join table that maps the relationship.
29 * @param string|null $inversedBy The name of the association-field on the inverse-side.
30 * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY'|null $fetch
31 */
32 public function __construct(
33 public readonly string $name,
34 array|JoinColumn|null $joinColumns = null,
35 array|JoinColumn|null $inverseJoinColumns = null,
36 public readonly JoinTable|null $joinTable = null,
37 public readonly string|null $inversedBy = null,
38 public readonly string|null $fetch = null,
39 ) {
40 if ($joinColumns instanceof JoinColumn) {
41 $joinColumns = [$joinColumns];
42 }
43
44 if ($inverseJoinColumns instanceof JoinColumn) {
45 $inverseJoinColumns = [$inverseJoinColumns];
46 }
47
48 $this->joinColumns = $joinColumns;
49 $this->inverseJoinColumns = $inverseJoinColumns;
50 }
51}
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php b/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php
new file mode 100644
index 0000000..9fc6807
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php
@@ -0,0 +1,38 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9use function array_values;
10use function is_array;
11
12/** This attribute is used to override association mappings of relationship properties. */
13#[Attribute(Attribute::TARGET_CLASS)]
14final class AssociationOverrides implements MappingAttribute
15{
16 /**
17 * Mapping overrides of relationship properties.
18 *
19 * @var list<AssociationOverride>
20 */
21 public readonly array $overrides;
22
23 /** @param array<AssociationOverride>|AssociationOverride $overrides */
24 public function __construct(array|AssociationOverride $overrides)
25 {
26 if (! is_array($overrides)) {
27 $overrides = [$overrides];
28 }
29
30 foreach ($overrides as $override) {
31 if (! ($override instanceof AssociationOverride)) {
32 throw MappingException::invalidOverrideType('AssociationOverride', $override);
33 }
34 }
35
36 $this->overrides = array_values($overrides);
37 }
38}
diff --git a/vendor/doctrine/orm/src/Mapping/AttributeOverride.php b/vendor/doctrine/orm/src/Mapping/AttributeOverride.php
new file mode 100644
index 0000000..8f0e70c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AttributeOverride.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/** This attribute is used to override the mapping of a entity property. */
8final class AttributeOverride implements MappingAttribute
9{
10 public function __construct(
11 public string $name,
12 public Column $column,
13 ) {
14 }
15}
diff --git a/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php b/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php
new file mode 100644
index 0000000..9c7b9db
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php
@@ -0,0 +1,38 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9use function array_values;
10use function is_array;
11
12/** This attribute is used to override the mapping of a entity property. */
13#[Attribute(Attribute::TARGET_CLASS)]
14final class AttributeOverrides implements MappingAttribute
15{
16 /**
17 * One or more field or property mapping overrides.
18 *
19 * @var list<AttributeOverride>
20 */
21 public readonly array $overrides;
22
23 /** @param array<AttributeOverride>|AttributeOverride $overrides */
24 public function __construct(array|AttributeOverride $overrides)
25 {
26 if (! is_array($overrides)) {
27 $overrides = [$overrides];
28 }
29
30 foreach ($overrides as $override) {
31 if (! ($override instanceof AttributeOverride)) {
32 throw MappingException::invalidOverrideType('AttributeOverride', $override);
33 }
34 }
35
36 $this->overrides = array_values($overrides);
37 }
38}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php
new file mode 100644
index 0000000..ea9e13c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php
@@ -0,0 +1,171 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7use Doctrine\ORM\Mapping\ClassMetadata;
8use InvalidArgumentException;
9
10class AssociationBuilder
11{
12 /** @var mixed[]|null */
13 protected array|null $joinColumns = null;
14
15 /** @param mixed[] $mapping */
16 public function __construct(
17 protected readonly ClassMetadataBuilder $builder,
18 protected array $mapping,
19 protected readonly int $type,
20 ) {
21 }
22
23 /** @return $this */
24 public function mappedBy(string $fieldName): static
25 {
26 $this->mapping['mappedBy'] = $fieldName;
27
28 return $this;
29 }
30
31 /** @return $this */
32 public function inversedBy(string $fieldName): static
33 {
34 $this->mapping['inversedBy'] = $fieldName;
35
36 return $this;
37 }
38
39 /** @return $this */
40 public function cascadeAll(): static
41 {
42 $this->mapping['cascade'] = ['ALL'];
43
44 return $this;
45 }
46
47 /** @return $this */
48 public function cascadePersist(): static
49 {
50 $this->mapping['cascade'][] = 'persist';
51
52 return $this;
53 }
54
55 /** @return $this */
56 public function cascadeRemove(): static
57 {
58 $this->mapping['cascade'][] = 'remove';
59
60 return $this;
61 }
62
63 /** @return $this */
64 public function cascadeDetach(): static
65 {
66 $this->mapping['cascade'][] = 'detach';
67
68 return $this;
69 }
70
71 /** @return $this */
72 public function cascadeRefresh(): static
73 {
74 $this->mapping['cascade'][] = 'refresh';
75
76 return $this;
77 }
78
79 /** @return $this */
80 public function fetchExtraLazy(): static
81 {
82 $this->mapping['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY;
83
84 return $this;
85 }
86
87 /** @return $this */
88 public function fetchEager(): static
89 {
90 $this->mapping['fetch'] = ClassMetadata::FETCH_EAGER;
91
92 return $this;
93 }
94
95 /** @return $this */
96 public function fetchLazy(): static
97 {
98 $this->mapping['fetch'] = ClassMetadata::FETCH_LAZY;
99
100 return $this;
101 }
102
103 /**
104 * Add Join Columns.
105 *
106 * @return $this
107 */
108 public function addJoinColumn(
109 string $columnName,
110 string $referencedColumnName,
111 bool $nullable = true,
112 bool $unique = false,
113 string|null $onDelete = null,
114 string|null $columnDef = null,
115 ): static {
116 $this->joinColumns[] = [
117 'name' => $columnName,
118 'referencedColumnName' => $referencedColumnName,
119 'nullable' => $nullable,
120 'unique' => $unique,
121 'onDelete' => $onDelete,
122 'columnDefinition' => $columnDef,
123 ];
124
125 return $this;
126 }
127
128 /**
129 * Sets field as primary key.
130 *
131 * @return $this
132 */
133 public function makePrimaryKey(): static
134 {
135 $this->mapping['id'] = true;
136
137 return $this;
138 }
139
140 /**
141 * Removes orphan entities when detached from their parent.
142 *
143 * @return $this
144 */
145 public function orphanRemoval(): static
146 {
147 $this->mapping['orphanRemoval'] = true;
148
149 return $this;
150 }
151
152 /** @throws InvalidArgumentException */
153 public function build(): ClassMetadataBuilder
154 {
155 $mapping = $this->mapping;
156 if ($this->joinColumns) {
157 $mapping['joinColumns'] = $this->joinColumns;
158 }
159
160 $cm = $this->builder->getClassMetadata();
161 if ($this->type === ClassMetadata::MANY_TO_ONE) {
162 $cm->mapManyToOne($mapping);
163 } elseif ($this->type === ClassMetadata::ONE_TO_ONE) {
164 $cm->mapOneToOne($mapping);
165 } else {
166 throw new InvalidArgumentException('Type should be a ToOne Association here');
167 }
168
169 return $this->builder;
170 }
171}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php
new file mode 100644
index 0000000..b9d3cc8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php
@@ -0,0 +1,426 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7use BackedEnum;
8use Doctrine\ORM\Mapping\ClassMetadata;
9
10/**
11 * Builder Object for ClassMetadata
12 *
13 * @link www.doctrine-project.com
14 */
15class ClassMetadataBuilder
16{
17 public function __construct(
18 private readonly ClassMetadata $cm,
19 ) {
20 }
21
22 public function getClassMetadata(): ClassMetadata
23 {
24 return $this->cm;
25 }
26
27 /**
28 * Marks the class as mapped superclass.
29 *
30 * @return $this
31 */
32 public function setMappedSuperClass(): static
33 {
34 $this->cm->isMappedSuperclass = true;
35 $this->cm->isEmbeddedClass = false;
36
37 return $this;
38 }
39
40 /**
41 * Marks the class as embeddable.
42 *
43 * @return $this
44 */
45 public function setEmbeddable(): static
46 {
47 $this->cm->isEmbeddedClass = true;
48 $this->cm->isMappedSuperclass = false;
49
50 return $this;
51 }
52
53 /**
54 * Adds and embedded class
55 *
56 * @param class-string $class
57 *
58 * @return $this
59 */
60 public function addEmbedded(string $fieldName, string $class, string|false|null $columnPrefix = null): static
61 {
62 $this->cm->mapEmbedded(
63 [
64 'fieldName' => $fieldName,
65 'class' => $class,
66 'columnPrefix' => $columnPrefix,
67 ],
68 );
69
70 return $this;
71 }
72
73 /**
74 * Sets custom Repository class name.
75 *
76 * @return $this
77 */
78 public function setCustomRepositoryClass(string $repositoryClassName): static
79 {
80 $this->cm->setCustomRepositoryClass($repositoryClassName);
81
82 return $this;
83 }
84
85 /**
86 * Marks class read only.
87 *
88 * @return $this
89 */
90 public function setReadOnly(): static
91 {
92 $this->cm->markReadOnly();
93
94 return $this;
95 }
96
97 /**
98 * Sets the table name.
99 *
100 * @return $this
101 */
102 public function setTable(string $name): static
103 {
104 $this->cm->setPrimaryTable(['name' => $name]);
105
106 return $this;
107 }
108
109 /**
110 * Adds Index.
111 *
112 * @psalm-param list<string> $columns
113 *
114 * @return $this
115 */
116 public function addIndex(array $columns, string $name): static
117 {
118 if (! isset($this->cm->table['indexes'])) {
119 $this->cm->table['indexes'] = [];
120 }
121
122 $this->cm->table['indexes'][$name] = ['columns' => $columns];
123
124 return $this;
125 }
126
127 /**
128 * Adds Unique Constraint.
129 *
130 * @psalm-param list<string> $columns
131 *
132 * @return $this
133 */
134 public function addUniqueConstraint(array $columns, string $name): static
135 {
136 if (! isset($this->cm->table['uniqueConstraints'])) {
137 $this->cm->table['uniqueConstraints'] = [];
138 }
139
140 $this->cm->table['uniqueConstraints'][$name] = ['columns' => $columns];
141
142 return $this;
143 }
144
145 /**
146 * Sets class as root of a joined table inheritance hierarchy.
147 *
148 * @return $this
149 */
150 public function setJoinedTableInheritance(): static
151 {
152 $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_JOINED);
153
154 return $this;
155 }
156
157 /**
158 * Sets class as root of a single table inheritance hierarchy.
159 *
160 * @return $this
161 */
162 public function setSingleTableInheritance(): static
163 {
164 $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE);
165
166 return $this;
167 }
168
169 /**
170 * Sets the discriminator column details.
171 *
172 * @psalm-param class-string<BackedEnum>|null $enumType
173 * @psalm-param array<string, mixed> $options
174 *
175 * @return $this
176 */
177 public function setDiscriminatorColumn(
178 string $name,
179 string $type = 'string',
180 int $length = 255,
181 string|null $columnDefinition = null,
182 string|null $enumType = null,
183 array $options = [],
184 ): static {
185 $this->cm->setDiscriminatorColumn(
186 [
187 'name' => $name,
188 'type' => $type,
189 'length' => $length,
190 'columnDefinition' => $columnDefinition,
191 'enumType' => $enumType,
192 'options' => $options,
193 ],
194 );
195
196 return $this;
197 }
198
199 /**
200 * Adds a subclass to this inheritance hierarchy.
201 *
202 * @return $this
203 */
204 public function addDiscriminatorMapClass(string $name, string $class): static
205 {
206 $this->cm->addDiscriminatorMapClass($name, $class);
207
208 return $this;
209 }
210
211 /**
212 * Sets deferred explicit change tracking policy.
213 *
214 * @return $this
215 */
216 public function setChangeTrackingPolicyDeferredExplicit(): static
217 {
218 $this->cm->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT);
219
220 return $this;
221 }
222
223 /**
224 * Adds lifecycle event.
225 *
226 * @return $this
227 */
228 public function addLifecycleEvent(string $methodName, string $event): static
229 {
230 $this->cm->addLifecycleCallback($methodName, $event);
231
232 return $this;
233 }
234
235 /**
236 * Adds Field.
237 *
238 * @psalm-param array<string, mixed> $mapping
239 *
240 * @return $this
241 */
242 public function addField(string $name, string $type, array $mapping = []): static
243 {
244 $mapping['fieldName'] = $name;
245 $mapping['type'] = $type;
246
247 $this->cm->mapField($mapping);
248
249 return $this;
250 }
251
252 /**
253 * Creates a field builder.
254 */
255 public function createField(string $name, string $type): FieldBuilder
256 {
257 return new FieldBuilder(
258 $this,
259 [
260 'fieldName' => $name,
261 'type' => $type,
262 ],
263 );
264 }
265
266 /**
267 * Creates an embedded builder.
268 */
269 public function createEmbedded(string $fieldName, string $class): EmbeddedBuilder
270 {
271 return new EmbeddedBuilder(
272 $this,
273 [
274 'fieldName' => $fieldName,
275 'class' => $class,
276 'columnPrefix' => null,
277 ],
278 );
279 }
280
281 /**
282 * Adds a simple many to one association, optionally with the inversed by field.
283 */
284 public function addManyToOne(
285 string $name,
286 string $targetEntity,
287 string|null $inversedBy = null,
288 ): ClassMetadataBuilder {
289 $builder = $this->createManyToOne($name, $targetEntity);
290
291 if ($inversedBy !== null) {
292 $builder->inversedBy($inversedBy);
293 }
294
295 return $builder->build();
296 }
297
298 /**
299 * Creates a ManyToOne Association Builder.
300 *
301 * Note: This method does not add the association, you have to call build() on the AssociationBuilder.
302 */
303 public function createManyToOne(string $name, string $targetEntity): AssociationBuilder
304 {
305 return new AssociationBuilder(
306 $this,
307 [
308 'fieldName' => $name,
309 'targetEntity' => $targetEntity,
310 ],
311 ClassMetadata::MANY_TO_ONE,
312 );
313 }
314
315 /**
316 * Creates a OneToOne Association Builder.
317 */
318 public function createOneToOne(string $name, string $targetEntity): AssociationBuilder
319 {
320 return new AssociationBuilder(
321 $this,
322 [
323 'fieldName' => $name,
324 'targetEntity' => $targetEntity,
325 ],
326 ClassMetadata::ONE_TO_ONE,
327 );
328 }
329
330 /**
331 * Adds simple inverse one-to-one association.
332 */
333 public function addInverseOneToOne(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
334 {
335 $builder = $this->createOneToOne($name, $targetEntity);
336 $builder->mappedBy($mappedBy);
337
338 return $builder->build();
339 }
340
341 /**
342 * Adds simple owning one-to-one association.
343 */
344 public function addOwningOneToOne(
345 string $name,
346 string $targetEntity,
347 string|null $inversedBy = null,
348 ): ClassMetadataBuilder {
349 $builder = $this->createOneToOne($name, $targetEntity);
350
351 if ($inversedBy !== null) {
352 $builder->inversedBy($inversedBy);
353 }
354
355 return $builder->build();
356 }
357
358 /**
359 * Creates a ManyToMany Association Builder.
360 */
361 public function createManyToMany(string $name, string $targetEntity): ManyToManyAssociationBuilder
362 {
363 return new ManyToManyAssociationBuilder(
364 $this,
365 [
366 'fieldName' => $name,
367 'targetEntity' => $targetEntity,
368 ],
369 ClassMetadata::MANY_TO_MANY,
370 );
371 }
372
373 /**
374 * Adds a simple owning many to many association.
375 */
376 public function addOwningManyToMany(
377 string $name,
378 string $targetEntity,
379 string|null $inversedBy = null,
380 ): ClassMetadataBuilder {
381 $builder = $this->createManyToMany($name, $targetEntity);
382
383 if ($inversedBy !== null) {
384 $builder->inversedBy($inversedBy);
385 }
386
387 return $builder->build();
388 }
389
390 /**
391 * Adds a simple inverse many to many association.
392 */
393 public function addInverseManyToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
394 {
395 $builder = $this->createManyToMany($name, $targetEntity);
396 $builder->mappedBy($mappedBy);
397
398 return $builder->build();
399 }
400
401 /**
402 * Creates a one to many association builder.
403 */
404 public function createOneToMany(string $name, string $targetEntity): OneToManyAssociationBuilder
405 {
406 return new OneToManyAssociationBuilder(
407 $this,
408 [
409 'fieldName' => $name,
410 'targetEntity' => $targetEntity,
411 ],
412 ClassMetadata::ONE_TO_MANY,
413 );
414 }
415
416 /**
417 * Adds simple OneToMany association.
418 */
419 public function addOneToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
420 {
421 $builder = $this->createOneToMany($name, $targetEntity);
422 $builder->mappedBy($mappedBy);
423
424 return $builder->build();
425 }
426}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php
new file mode 100644
index 0000000..b9d2127
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7/**
8 * Embedded Builder
9 *
10 * @link www.doctrine-project.com
11 */
12class EmbeddedBuilder
13{
14 /** @param mixed[] $mapping */
15 public function __construct(
16 private readonly ClassMetadataBuilder $builder,
17 private array $mapping,
18 ) {
19 }
20
21 /**
22 * Sets the column prefix for all of the embedded columns.
23 *
24 * @return $this
25 */
26 public function setColumnPrefix(string $columnPrefix): static
27 {
28 $this->mapping['columnPrefix'] = $columnPrefix;
29
30 return $this;
31 }
32
33 /**
34 * Finalizes this embeddable and attach it to the ClassMetadata.
35 *
36 * Without this call an EmbeddedBuilder has no effect on the ClassMetadata.
37 */
38 public function build(): ClassMetadataBuilder
39 {
40 $cm = $this->builder->getClassMetadata();
41
42 $cm->mapEmbedded($this->mapping);
43
44 return $this->builder;
45 }
46}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php
new file mode 100644
index 0000000..a0b14b9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php
@@ -0,0 +1,55 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7use Doctrine\ORM\Events;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\Mapping\MappingException;
10
11use function class_exists;
12use function get_class_methods;
13
14/**
15 * Builder for entity listeners.
16 */
17class EntityListenerBuilder
18{
19 /** Hash-map to handle event names. */
20 private const EVENTS = [
21 Events::preRemove => true,
22 Events::postRemove => true,
23 Events::prePersist => true,
24 Events::postPersist => true,
25 Events::preUpdate => true,
26 Events::postUpdate => true,
27 Events::postLoad => true,
28 Events::preFlush => true,
29 ];
30
31 /**
32 * Lookup the entity class to find methods that match to event lifecycle names
33 *
34 * @param ClassMetadata $metadata The entity metadata.
35 * @param string $className The listener class name.
36 *
37 * @throws MappingException When the listener class not found.
38 */
39 public static function bindEntityListener(ClassMetadata $metadata, string $className): void
40 {
41 $class = $metadata->fullyQualifiedClassName($className);
42
43 if (! class_exists($class)) {
44 throw MappingException::entityListenerClassNotFound($class, $className);
45 }
46
47 foreach (get_class_methods($class) as $method) {
48 if (! isset(self::EVENTS[$method])) {
49 continue;
50 }
51
52 $metadata->addEntityListener($method, $class, $method);
53 }
54 }
55}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php
new file mode 100644
index 0000000..8326ff5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php
@@ -0,0 +1,243 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7use function constant;
8
9/**
10 * Field Builder
11 *
12 * @link www.doctrine-project.com
13 */
14class FieldBuilder
15{
16 private bool $version = false;
17 private string|null $generatedValue = null;
18
19 /** @var mixed[]|null */
20 private array|null $sequenceDef = null;
21
22 private string|null $customIdGenerator = null;
23
24 /** @param mixed[] $mapping */
25 public function __construct(
26 private readonly ClassMetadataBuilder $builder,
27 private array $mapping,
28 ) {
29 }
30
31 /**
32 * Sets length.
33 *
34 * @return $this
35 */
36 public function length(int $length): static
37 {
38 $this->mapping['length'] = $length;
39
40 return $this;
41 }
42
43 /**
44 * Sets nullable.
45 *
46 * @return $this
47 */
48 public function nullable(bool $flag = true): static
49 {
50 $this->mapping['nullable'] = $flag;
51
52 return $this;
53 }
54
55 /**
56 * Sets Unique.
57 *
58 * @return $this
59 */
60 public function unique(bool $flag = true): static
61 {
62 $this->mapping['unique'] = $flag;
63
64 return $this;
65 }
66
67 /**
68 * Sets column name.
69 *
70 * @return $this
71 */
72 public function columnName(string $name): static
73 {
74 $this->mapping['columnName'] = $name;
75
76 return $this;
77 }
78
79 /**
80 * Sets Precision.
81 *
82 * @return $this
83 */
84 public function precision(int $p): static
85 {
86 $this->mapping['precision'] = $p;
87
88 return $this;
89 }
90
91 /**
92 * Sets insertable.
93 *
94 * @return $this
95 */
96 public function insertable(bool $flag = true): self
97 {
98 if (! $flag) {
99 $this->mapping['notInsertable'] = true;
100 }
101
102 return $this;
103 }
104
105 /**
106 * Sets updatable.
107 *
108 * @return $this
109 */
110 public function updatable(bool $flag = true): self
111 {
112 if (! $flag) {
113 $this->mapping['notUpdatable'] = true;
114 }
115
116 return $this;
117 }
118
119 /**
120 * Sets scale.
121 *
122 * @return $this
123 */
124 public function scale(int $s): static
125 {
126 $this->mapping['scale'] = $s;
127
128 return $this;
129 }
130
131 /**
132 * Sets field as primary key.
133 *
134 * @return $this
135 */
136 public function makePrimaryKey(): static
137 {
138 $this->mapping['id'] = true;
139
140 return $this;
141 }
142
143 /**
144 * Sets an option.
145 *
146 * @return $this
147 */
148 public function option(string $name, mixed $value): static
149 {
150 $this->mapping['options'][$name] = $value;
151
152 return $this;
153 }
154
155 /** @return $this */
156 public function generatedValue(string $strategy = 'AUTO'): static
157 {
158 $this->generatedValue = $strategy;
159
160 return $this;
161 }
162
163 /**
164 * Sets field versioned.
165 *
166 * @return $this
167 */
168 public function isVersionField(): static
169 {
170 $this->version = true;
171
172 return $this;
173 }
174
175 /**
176 * Sets Sequence Generator.
177 *
178 * @return $this
179 */
180 public function setSequenceGenerator(string $sequenceName, int $allocationSize = 1, int $initialValue = 1): static
181 {
182 $this->sequenceDef = [
183 'sequenceName' => $sequenceName,
184 'allocationSize' => $allocationSize,
185 'initialValue' => $initialValue,
186 ];
187
188 return $this;
189 }
190
191 /**
192 * Sets column definition.
193 *
194 * @return $this
195 */
196 public function columnDefinition(string $def): static
197 {
198 $this->mapping['columnDefinition'] = $def;
199
200 return $this;
201 }
202
203 /**
204 * Set the FQCN of the custom ID generator.
205 * This class must extend \Doctrine\ORM\Id\AbstractIdGenerator.
206 *
207 * @return $this
208 */
209 public function setCustomIdGenerator(string $customIdGenerator): static
210 {
211 $this->customIdGenerator = $customIdGenerator;
212
213 return $this;
214 }
215
216 /**
217 * Finalizes this field and attach it to the ClassMetadata.
218 *
219 * Without this call a FieldBuilder has no effect on the ClassMetadata.
220 */
221 public function build(): ClassMetadataBuilder
222 {
223 $cm = $this->builder->getClassMetadata();
224 if ($this->generatedValue) {
225 $cm->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $this->generatedValue));
226 }
227
228 if ($this->version) {
229 $cm->setVersionMapping($this->mapping);
230 }
231
232 $cm->mapField($this->mapping);
233 if ($this->sequenceDef) {
234 $cm->setSequenceGeneratorDefinition($this->sequenceDef);
235 }
236
237 if ($this->customIdGenerator) {
238 $cm->setCustomGeneratorDefinition(['class' => $this->customIdGenerator]);
239 }
240
241 return $this->builder;
242 }
243}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php
new file mode 100644
index 0000000..b83a8ba
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php
@@ -0,0 +1,73 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7/**
8 * ManyToMany Association Builder
9 *
10 * @link www.doctrine-project.com
11 */
12class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
13{
14 private string|null $joinTableName = null;
15
16 /** @var mixed[] */
17 private array $inverseJoinColumns = [];
18
19 /** @return $this */
20 public function setJoinTable(string $name): static
21 {
22 $this->joinTableName = $name;
23
24 return $this;
25 }
26
27 /**
28 * Adds Inverse Join Columns.
29 *
30 * @return $this
31 */
32 public function addInverseJoinColumn(
33 string $columnName,
34 string $referencedColumnName,
35 bool $nullable = true,
36 bool $unique = false,
37 string|null $onDelete = null,
38 string|null $columnDef = null,
39 ): static {
40 $this->inverseJoinColumns[] = [
41 'name' => $columnName,
42 'referencedColumnName' => $referencedColumnName,
43 'nullable' => $nullable,
44 'unique' => $unique,
45 'onDelete' => $onDelete,
46 'columnDefinition' => $columnDef,
47 ];
48
49 return $this;
50 }
51
52 public function build(): ClassMetadataBuilder
53 {
54 $mapping = $this->mapping;
55 $mapping['joinTable'] = [];
56 if ($this->joinColumns) {
57 $mapping['joinTable']['joinColumns'] = $this->joinColumns;
58 }
59
60 if ($this->inverseJoinColumns) {
61 $mapping['joinTable']['inverseJoinColumns'] = $this->inverseJoinColumns;
62 }
63
64 if ($this->joinTableName) {
65 $mapping['joinTable']['name'] = $this->joinTableName;
66 }
67
68 $cm = $this->builder->getClassMetadata();
69 $cm->mapManyToMany($mapping);
70
71 return $this->builder;
72 }
73}
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php
new file mode 100644
index 0000000..077c558
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Builder;
6
7/**
8 * OneToMany Association Builder
9 *
10 * @link www.doctrine-project.com
11 */
12class OneToManyAssociationBuilder extends AssociationBuilder
13{
14 /**
15 * @psalm-param array<string, string> $fieldNames
16 *
17 * @return $this
18 */
19 public function setOrderBy(array $fieldNames): static
20 {
21 $this->mapping['orderBy'] = $fieldNames;
22
23 return $this;
24 }
25
26 /** @return $this */
27 public function setIndexBy(string $fieldName): static
28 {
29 $this->mapping['indexBy'] = $fieldName;
30
31 return $this;
32 }
33
34 public function build(): ClassMetadataBuilder
35 {
36 $mapping = $this->mapping;
37 if ($this->joinColumns) {
38 $mapping['joinColumns'] = $this->joinColumns;
39 }
40
41 $cm = $this->builder->getClassMetadata();
42 $cm->mapOneToMany($mapping);
43
44 return $this->builder;
45 }
46}
diff --git a/vendor/doctrine/orm/src/Mapping/Cache.php b/vendor/doctrine/orm/src/Mapping/Cache.php
new file mode 100644
index 0000000..3161ab3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Cache.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9/** Caching to an entity or a collection. */
10#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
11final class Cache implements MappingAttribute
12{
13 /** @psalm-param 'READ_ONLY'|'NONSTRICT_READ_WRITE'|'READ_WRITE' $usage */
14 public function __construct(
15 public readonly string $usage = 'READ_ONLY',
16 public readonly string|null $region = null,
17 ) {
18 }
19}
diff --git a/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php
new file mode 100644
index 0000000..ed1ba93
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\ORM\Internal\NoUnknownNamedArguments;
8use ReflectionProperty;
9
10final class ChainTypedFieldMapper implements TypedFieldMapper
11{
12 use NoUnknownNamedArguments;
13
14 /** @var list<TypedFieldMapper> $typedFieldMappers */
15 private readonly array $typedFieldMappers;
16
17 public function __construct(TypedFieldMapper ...$typedFieldMappers)
18 {
19 self::validateVariadicParameter($typedFieldMappers);
20
21 $this->typedFieldMappers = $typedFieldMappers;
22 }
23
24 /**
25 * {@inheritDoc}
26 */
27 public function validateAndComplete(array $mapping, ReflectionProperty $field): array
28 {
29 foreach ($this->typedFieldMappers as $typedFieldMapper) {
30 $mapping = $typedFieldMapper->validateAndComplete($mapping, $field);
31 }
32
33 return $mapping;
34 }
35}
diff --git a/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php b/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php
new file mode 100644
index 0000000..7181d9f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS)]
10final class ChangeTrackingPolicy implements MappingAttribute
11{
12 /** @psalm-param 'DEFERRED_IMPLICIT'|'DEFERRED_EXPLICIT' $value */
13 public function __construct(
14 public readonly string $value,
15 ) {
16 }
17}
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}
diff --git a/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php b/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php
new file mode 100644
index 0000000..a3a4701
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php
@@ -0,0 +1,729 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\Common\EventManager;
8use Doctrine\DBAL\Platforms;
9use Doctrine\DBAL\Platforms\AbstractPlatform;
10use Doctrine\Deprecations\Deprecation;
11use Doctrine\ORM\EntityManagerInterface;
12use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
13use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
14use Doctrine\ORM\Events;
15use Doctrine\ORM\Exception\ORMException;
16use Doctrine\ORM\Id\AssignedGenerator;
17use Doctrine\ORM\Id\BigIntegerIdentityGenerator;
18use Doctrine\ORM\Id\IdentityGenerator;
19use Doctrine\ORM\Id\SequenceGenerator;
20use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
21use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
22use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
23use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
24use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
25use Doctrine\Persistence\Mapping\Driver\MappingDriver;
26use Doctrine\Persistence\Mapping\ReflectionService;
27use ReflectionClass;
28use ReflectionException;
29
30use function assert;
31use function class_exists;
32use function count;
33use function end;
34use function explode;
35use function in_array;
36use function is_a;
37use function is_subclass_of;
38use function method_exists;
39use function str_contains;
40use function strlen;
41use function strtolower;
42use function substr;
43
44/**
45 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
46 * metadata mapping information of a class which describes how a class should be mapped
47 * to a relational database.
48 *
49 * @extends AbstractClassMetadataFactory<ClassMetadata>
50 */
51class ClassMetadataFactory extends AbstractClassMetadataFactory
52{
53 private EntityManagerInterface|null $em = null;
54 private AbstractPlatform|null $targetPlatform = null;
55 private MappingDriver|null $driver = null;
56 private EventManager|null $evm = null;
57
58 /** @var mixed[] */
59 private array $embeddablesActiveNesting = [];
60
61 private const NON_IDENTITY_DEFAULT_STRATEGY = [
62 Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
63 ];
64
65 public function setEntityManager(EntityManagerInterface $em): void
66 {
67 parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
68
69 $this->em = $em;
70 }
71
72 /**
73 * @param A $maybeOwningSide
74 *
75 * @return (A is ManyToManyAssociationMapping ? ManyToManyOwningSideMapping : (
76 * A is OneToOneAssociationMapping ? OneToOneOwningSideMapping : (
77 * A is OneToManyAssociationMapping ? ManyToOneAssociationMapping : (
78 * A is ManyToOneAssociationMapping ? ManyToOneAssociationMapping :
79 * ManyToManyOwningSideMapping|OneToOneOwningSideMapping|ManyToOneAssociationMapping
80 * ))))
81 *
82 * @template A of AssociationMapping
83 */
84 final public function getOwningSide(AssociationMapping $maybeOwningSide): OwningSideMapping
85 {
86 if ($maybeOwningSide instanceof OwningSideMapping) {
87 assert($maybeOwningSide instanceof ManyToManyOwningSideMapping ||
88 $maybeOwningSide instanceof OneToOneOwningSideMapping ||
89 $maybeOwningSide instanceof ManyToOneAssociationMapping);
90
91 return $maybeOwningSide;
92 }
93
94 assert($maybeOwningSide instanceof InverseSideMapping);
95
96 $owningSide = $this->getMetadataFor($maybeOwningSide->targetEntity)
97 ->associationMappings[$maybeOwningSide->mappedBy];
98
99 assert($owningSide instanceof ManyToManyOwningSideMapping ||
100 $owningSide instanceof OneToOneOwningSideMapping ||
101 $owningSide instanceof ManyToOneAssociationMapping);
102
103 return $owningSide;
104 }
105
106 protected function initialize(): void
107 {
108 $this->driver = $this->em->getConfiguration()->getMetadataDriverImpl();
109 $this->evm = $this->em->getEventManager();
110 $this->initialized = true;
111 }
112
113 protected function onNotFoundMetadata(string $className): ClassMetadata|null
114 {
115 if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
116 return null;
117 }
118
119 $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em);
120
121 $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
122 $classMetadata = $eventArgs->getFoundMetadata();
123 assert($classMetadata instanceof ClassMetadata || $classMetadata === null);
124
125 return $classMetadata;
126 }
127
128 /**
129 * {@inheritDoc}
130 */
131 protected function doLoadMetadata(
132 ClassMetadataInterface $class,
133 ClassMetadataInterface|null $parent,
134 bool $rootEntityFound,
135 array $nonSuperclassParents,
136 ): void {
137 if ($parent) {
138 $class->setInheritanceType($parent->inheritanceType);
139 $class->setDiscriminatorColumn($parent->discriminatorColumn === null ? null : clone $parent->discriminatorColumn);
140 $class->setIdGeneratorType($parent->generatorType);
141 $this->addInheritedFields($class, $parent);
142 $this->addInheritedRelations($class, $parent);
143 $this->addInheritedEmbeddedClasses($class, $parent);
144 $class->setIdentifier($parent->identifier);
145 $class->setVersioned($parent->isVersioned);
146 $class->setVersionField($parent->versionField);
147 $class->setDiscriminatorMap($parent->discriminatorMap);
148 $class->addSubClasses($parent->subClasses);
149 $class->setLifecycleCallbacks($parent->lifecycleCallbacks);
150 $class->setChangeTrackingPolicy($parent->changeTrackingPolicy);
151
152 if (! empty($parent->customGeneratorDefinition)) {
153 $class->setCustomGeneratorDefinition($parent->customGeneratorDefinition);
154 }
155
156 if ($parent->isMappedSuperclass) {
157 $class->setCustomRepositoryClass($parent->customRepositoryClassName);
158 }
159 }
160
161 // Invoke driver
162 try {
163 $this->driver->loadMetadataForClass($class->getName(), $class);
164 } catch (ReflectionException $e) {
165 throw MappingException::reflectionFailure($class->getName(), $e);
166 }
167
168 // If this class has a parent the id generator strategy is inherited.
169 // However this is only true if the hierarchy of parents contains the root entity,
170 // if it consists of mapped superclasses these don't necessarily include the id field.
171 if ($parent && $rootEntityFound) {
172 $this->inheritIdGeneratorMapping($class, $parent);
173 } else {
174 $this->completeIdGeneratorMapping($class);
175 }
176
177 if (! $class->isMappedSuperclass) {
178 if ($rootEntityFound && $class->isInheritanceTypeNone()) {
179 throw MappingException::missingInheritanceTypeDeclaration(end($nonSuperclassParents), $class->name);
180 }
181
182 foreach ($class->embeddedClasses as $property => $embeddableClass) {
183 if (isset($embeddableClass->inherited)) {
184 continue;
185 }
186
187 if (isset($this->embeddablesActiveNesting[$embeddableClass->class])) {
188 throw MappingException::infiniteEmbeddableNesting($class->name, $property);
189 }
190
191 $this->embeddablesActiveNesting[$class->name] = true;
192
193 $embeddableMetadata = $this->getMetadataFor($embeddableClass->class);
194
195 if ($embeddableMetadata->isEmbeddedClass) {
196 $this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property);
197 }
198
199 $identifier = $embeddableMetadata->getIdentifier();
200
201 if (! empty($identifier)) {
202 $this->inheritIdGeneratorMapping($class, $embeddableMetadata);
203 }
204
205 $class->inlineEmbeddable($property, $embeddableMetadata);
206
207 unset($this->embeddablesActiveNesting[$class->name]);
208 }
209 }
210
211 if ($parent) {
212 if ($parent->isInheritanceTypeSingleTable()) {
213 $class->setPrimaryTable($parent->table);
214 }
215
216 $this->addInheritedIndexes($class, $parent);
217
218 if ($parent->cache) {
219 $class->cache = $parent->cache;
220 }
221
222 if ($parent->containsForeignIdentifier) {
223 $class->containsForeignIdentifier = true;
224 }
225
226 if ($parent->containsEnumIdentifier) {
227 $class->containsEnumIdentifier = true;
228 }
229
230 if (! empty($parent->entityListeners) && empty($class->entityListeners)) {
231 $class->entityListeners = $parent->entityListeners;
232 }
233 }
234
235 $class->setParentClasses($nonSuperclassParents);
236
237 if ($class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) {
238 $this->addDefaultDiscriminatorMap($class);
239 }
240
241 // During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402.
242 // So, we must not discover the missing subclasses before that.
243
244 if ($this->evm->hasListeners(Events::loadClassMetadata)) {
245 $eventArgs = new LoadClassMetadataEventArgs($class, $this->em);
246 $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
247 }
248
249 $this->findAbstractEntityClassesNotListedInDiscriminatorMap($class);
250
251 $this->validateRuntimeMetadata($class, $parent);
252 }
253
254 /**
255 * Validate runtime metadata is correctly defined.
256 *
257 * @throws MappingException
258 */
259 protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataInterface|null $parent): void
260 {
261 if (! $class->reflClass) {
262 // only validate if there is a reflection class instance
263 return;
264 }
265
266 $class->validateIdentifier();
267 $class->validateAssociations();
268 $class->validateLifecycleCallbacks($this->getReflectionService());
269
270 // verify inheritance
271 if (! $class->isMappedSuperclass && ! $class->isInheritanceTypeNone()) {
272 if (! $parent) {
273 if (count($class->discriminatorMap) === 0) {
274 throw MappingException::missingDiscriminatorMap($class->name);
275 }
276
277 if (! $class->discriminatorColumn) {
278 throw MappingException::missingDiscriminatorColumn($class->name);
279 }
280
281 foreach ($class->subClasses as $subClass) {
282 if ((new ReflectionClass($subClass))->name !== $subClass) {
283 throw MappingException::invalidClassInDiscriminatorMap($subClass, $class->name);
284 }
285 }
286 } else {
287 assert($parent instanceof ClassMetadata); // https://github.com/doctrine/orm/issues/8746
288 if (
289 ! $class->reflClass->isAbstract()
290 && ! in_array($class->name, $class->discriminatorMap, true)
291 ) {
292 throw MappingException::mappedClassNotPartOfDiscriminatorMap($class->name, $class->rootEntityName);
293 }
294 }
295 } elseif ($class->isMappedSuperclass && $class->name === $class->rootEntityName && (count($class->discriminatorMap) || $class->discriminatorColumn)) {
296 // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
297 throw MappingException::noInheritanceOnMappedSuperClass($class->name);
298 }
299 }
300
301 protected function newClassMetadataInstance(string $className): ClassMetadata
302 {
303 return new ClassMetadata(
304 $className,
305 $this->em->getConfiguration()->getNamingStrategy(),
306 $this->em->getConfiguration()->getTypedFieldMapper(),
307 );
308 }
309
310 /**
311 * Adds a default discriminator map if no one is given
312 *
313 * If an entity is of any inheritance type and does not contain a
314 * discriminator map, then the map is generated automatically. This process
315 * is expensive computation wise.
316 *
317 * The automatically generated discriminator map contains the lowercase short name of
318 * each class as key.
319 *
320 * @throws MappingException
321 */
322 private function addDefaultDiscriminatorMap(ClassMetadata $class): void
323 {
324 $allClasses = $this->driver->getAllClassNames();
325 $fqcn = $class->getName();
326 $map = [$this->getShortName($class->name) => $fqcn];
327
328 $duplicates = [];
329 foreach ($allClasses as $subClassCandidate) {
330 if (is_subclass_of($subClassCandidate, $fqcn)) {
331 $shortName = $this->getShortName($subClassCandidate);
332
333 if (isset($map[$shortName])) {
334 $duplicates[] = $shortName;
335 }
336
337 $map[$shortName] = $subClassCandidate;
338 }
339 }
340
341 if ($duplicates) {
342 throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map);
343 }
344
345 $class->setDiscriminatorMap($map);
346 }
347
348 private function findAbstractEntityClassesNotListedInDiscriminatorMap(ClassMetadata $rootEntityClass): void
349 {
350 // Only root classes in inheritance hierarchies need contain a discriminator map,
351 // so skip for other classes.
352 if (! $rootEntityClass->isRootEntity() || $rootEntityClass->isInheritanceTypeNone()) {
353 return;
354 }
355
356 $processedClasses = [$rootEntityClass->name => true];
357 foreach ($rootEntityClass->subClasses as $knownSubClass) {
358 $processedClasses[$knownSubClass] = true;
359 }
360
361 foreach ($rootEntityClass->discriminatorMap as $declaredClassName) {
362 // This fetches non-transient parent classes only
363 $parentClasses = $this->getParentClasses($declaredClassName);
364
365 foreach ($parentClasses as $parentClass) {
366 if (isset($processedClasses[$parentClass])) {
367 continue;
368 }
369
370 $processedClasses[$parentClass] = true;
371
372 // All non-abstract entity classes must be listed in the discriminator map, and
373 // this will be validated/enforced at runtime (possibly at a later time, when the
374 // subclass is loaded, but anyways). Also, subclasses is about entity classes only.
375 // That means we can ignore non-abstract classes here. The (expensive) driver
376 // check for mapped superclasses need only be run for abstract candidate classes.
377 if (! (new ReflectionClass($parentClass))->isAbstract() || $this->peekIfIsMappedSuperclass($parentClass)) {
378 continue;
379 }
380
381 // We have found a non-transient, non-mapped-superclass = an entity class (possibly abstract, but that does not matter)
382 $rootEntityClass->addSubClass($parentClass);
383 }
384 }
385 }
386
387 /** @param class-string $className */
388 private function peekIfIsMappedSuperclass(string $className): bool
389 {
390 $reflService = $this->getReflectionService();
391 $class = $this->newClassMetadataInstance($className);
392 $this->initializeReflection($class, $reflService);
393
394 $this->getDriver()->loadMetadataForClass($className, $class);
395
396 return $class->isMappedSuperclass;
397 }
398
399 /**
400 * Gets the lower-case short name of a class.
401 *
402 * @psalm-param class-string $className
403 */
404 private function getShortName(string $className): string
405 {
406 if (! str_contains($className, '\\')) {
407 return strtolower($className);
408 }
409
410 $parts = explode('\\', $className);
411
412 return strtolower(end($parts));
413 }
414
415 /**
416 * Puts the `inherited` and `declared` values into mapping information for fields, associations
417 * and embedded classes.
418 */
419 private function addMappingInheritanceInformation(
420 AssociationMapping|EmbeddedClassMapping|FieldMapping $mapping,
421 ClassMetadata $parentClass,
422 ): void {
423 if (! isset($mapping->inherited) && ! $parentClass->isMappedSuperclass) {
424 $mapping->inherited = $parentClass->name;
425 }
426
427 if (! isset($mapping->declared)) {
428 $mapping->declared = $parentClass->name;
429 }
430 }
431
432 /**
433 * Adds inherited fields to the subclass mapping.
434 */
435 private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
436 {
437 foreach ($parentClass->fieldMappings as $mapping) {
438 $subClassMapping = clone $mapping;
439 $this->addMappingInheritanceInformation($subClassMapping, $parentClass);
440 $subClass->addInheritedFieldMapping($subClassMapping);
441 }
442
443 foreach ($parentClass->reflFields as $name => $field) {
444 $subClass->reflFields[$name] = $field;
445 }
446 }
447
448 /**
449 * Adds inherited association mappings to the subclass mapping.
450 *
451 * @throws MappingException
452 */
453 private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void
454 {
455 foreach ($parentClass->associationMappings as $field => $mapping) {
456 $subClassMapping = clone $mapping;
457 $this->addMappingInheritanceInformation($subClassMapping, $parentClass);
458 // When the class inheriting the relation ($subClass) is the first entity class since the
459 // relation has been defined in a mapped superclass (or in a chain
460 // of mapped superclasses) above, then declare this current entity class as the source of
461 // the relationship.
462 // According to the definitions given in https://github.com/doctrine/orm/pull/10396/,
463 // this is the case <=> ! isset($mapping['inherited']).
464 if (! isset($subClassMapping->inherited)) {
465 $subClassMapping->sourceEntity = $subClass->name;
466 }
467
468 $subClass->addInheritedAssociationMapping($subClassMapping);
469 }
470 }
471
472 private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void
473 {
474 foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
475 $subClassMapping = clone $embeddedClass;
476 $this->addMappingInheritanceInformation($subClassMapping, $parentClass);
477 $subClass->embeddedClasses[$field] = $subClassMapping;
478 }
479 }
480
481 /**
482 * Adds nested embedded classes metadata to a parent class.
483 *
484 * @param ClassMetadata $subClass Sub embedded class metadata to add nested embedded classes metadata from.
485 * @param ClassMetadata $parentClass Parent class to add nested embedded classes metadata to.
486 * @param string $prefix Embedded classes' prefix to use for nested embedded classes field names.
487 */
488 private function addNestedEmbeddedClasses(
489 ClassMetadata $subClass,
490 ClassMetadata $parentClass,
491 string $prefix,
492 ): void {
493 foreach ($subClass->embeddedClasses as $property => $embeddableClass) {
494 if (isset($embeddableClass->inherited)) {
495 continue;
496 }
497
498 $embeddableMetadata = $this->getMetadataFor($embeddableClass->class);
499
500 $parentClass->mapEmbedded(
501 [
502 'fieldName' => $prefix . '.' . $property,
503 'class' => $embeddableMetadata->name,
504 'columnPrefix' => $embeddableClass->columnPrefix,
505 'declaredField' => $embeddableClass->declaredField
506 ? $prefix . '.' . $embeddableClass->declaredField
507 : $prefix,
508 'originalField' => $embeddableClass->originalField ?: $property,
509 ],
510 );
511 }
512 }
513
514 /**
515 * Copy the table indices from the parent class superclass to the child class
516 */
517 private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $parentClass): void
518 {
519 if (! $parentClass->isMappedSuperclass) {
520 return;
521 }
522
523 foreach (['uniqueConstraints', 'indexes'] as $indexType) {
524 if (isset($parentClass->table[$indexType])) {
525 foreach ($parentClass->table[$indexType] as $indexName => $index) {
526 if (isset($subClass->table[$indexType][$indexName])) {
527 continue; // Let the inheriting table override indices
528 }
529
530 $subClass->table[$indexType][$indexName] = $index;
531 }
532 }
533 }
534 }
535
536 /**
537 * Completes the ID generator mapping. If "auto" is specified we choose the generator
538 * most appropriate for the targeted database platform.
539 *
540 * @throws ORMException
541 */
542 private function completeIdGeneratorMapping(ClassMetadata $class): void
543 {
544 $idGenType = $class->generatorType;
545 if ($idGenType === ClassMetadata::GENERATOR_TYPE_AUTO) {
546 $class->setIdGeneratorType($this->determineIdGeneratorStrategy($this->getTargetPlatform()));
547 }
548
549 // Create & assign an appropriate ID generator instance
550 switch ($class->generatorType) {
551 case ClassMetadata::GENERATOR_TYPE_IDENTITY:
552 $sequenceName = null;
553 $fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null;
554 $platform = $this->getTargetPlatform();
555
556 $generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint'
557 ? new BigIntegerIdentityGenerator()
558 : new IdentityGenerator();
559
560 $class->setIdGenerator($generator);
561
562 break;
563
564 case ClassMetadata::GENERATOR_TYPE_SEQUENCE:
565 // If there is no sequence definition yet, create a default definition
566 $definition = $class->sequenceGeneratorDefinition;
567
568 if (! $definition) {
569 $fieldName = $class->getSingleIdentifierFieldName();
570 $sequenceName = $class->getSequenceName($this->getTargetPlatform());
571 $quoted = isset($class->fieldMappings[$fieldName]->quoted) || isset($class->table['quoted']);
572
573 $definition = [
574 'sequenceName' => $this->truncateSequenceName($sequenceName),
575 'allocationSize' => 1,
576 'initialValue' => 1,
577 ];
578
579 if ($quoted) {
580 $definition['quoted'] = true;
581 }
582
583 $class->setSequenceGeneratorDefinition($definition);
584 }
585
586 $sequenceGenerator = new SequenceGenerator(
587 $this->em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $this->getTargetPlatform()),
588 (int) $definition['allocationSize'],
589 );
590 $class->setIdGenerator($sequenceGenerator);
591 break;
592
593 case ClassMetadata::GENERATOR_TYPE_NONE:
594 $class->setIdGenerator(new AssignedGenerator());
595 break;
596
597 case ClassMetadata::GENERATOR_TYPE_CUSTOM:
598 $definition = $class->customGeneratorDefinition;
599 if ($definition === null) {
600 throw InvalidCustomGenerator::onClassNotConfigured();
601 }
602
603 if (! class_exists($definition['class'])) {
604 throw InvalidCustomGenerator::onMissingClass($definition);
605 }
606
607 $class->setIdGenerator(new $definition['class']());
608 break;
609
610 default:
611 throw UnknownGeneratorType::create($class->generatorType);
612 }
613 }
614
615 /** @psalm-return ClassMetadata::GENERATOR_TYPE_* */
616 private function determineIdGeneratorStrategy(AbstractPlatform $platform): int
617 {
618 assert($this->em !== null);
619 foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) {
620 if (is_a($platform, $platformFamily)) {
621 return $strategy;
622 }
623 }
624
625 $nonIdentityDefaultStrategy = self::NON_IDENTITY_DEFAULT_STRATEGY;
626
627 // DBAL 3
628 if (method_exists($platform, 'getIdentitySequenceName')) {
629 $nonIdentityDefaultStrategy[Platforms\PostgreSQLPlatform::class] = ClassMetadata::GENERATOR_TYPE_SEQUENCE;
630 }
631
632 foreach ($nonIdentityDefaultStrategy as $platformFamily => $strategy) {
633 if (is_a($platform, $platformFamily)) {
634 if ($platform instanceof Platforms\PostgreSQLPlatform) {
635 Deprecation::trigger(
636 'doctrine/orm',
637 'https://github.com/doctrine/orm/issues/8893',
638 <<<'DEPRECATION'
639 Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY
640 results in SERIAL, which is not recommended.
641 Instead, configure identifier generation strategies explicitly through
642 configuration.
643 We currently recommend "SEQUENCE" for "%s", when using DBAL 3,
644 and "IDENTITY" when using DBAL 4,
645 so you should probably use the following configuration before upgrading to DBAL 4,
646 and remove it after deploying that upgrade:
647
648 $configuration->setIdentityGenerationPreferences([
649 "%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
650 ]);
651
652 DEPRECATION,
653 $platformFamily,
654 $platformFamily,
655 );
656 }
657
658 return $strategy;
659 }
660 }
661
662 return ClassMetadata::GENERATOR_TYPE_IDENTITY;
663 }
664
665 private function truncateSequenceName(string $schemaElementName): string
666 {
667 $platform = $this->getTargetPlatform();
668 if (! $platform instanceof Platforms\OraclePlatform) {
669 return $schemaElementName;
670 }
671
672 $maxIdentifierLength = $platform->getMaxIdentifierLength();
673
674 if (strlen($schemaElementName) > $maxIdentifierLength) {
675 return substr($schemaElementName, 0, $maxIdentifierLength);
676 }
677
678 return $schemaElementName;
679 }
680
681 /**
682 * Inherits the ID generator mapping from a parent class.
683 */
684 private function inheritIdGeneratorMapping(ClassMetadata $class, ClassMetadata $parent): void
685 {
686 if ($parent->isIdGeneratorSequence()) {
687 $class->setSequenceGeneratorDefinition($parent->sequenceGeneratorDefinition);
688 }
689
690 if ($parent->generatorType) {
691 $class->setIdGeneratorType($parent->generatorType);
692 }
693
694 if ($parent->idGenerator ?? null) {
695 $class->setIdGenerator($parent->idGenerator);
696 }
697 }
698
699 protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
700 {
701 $class->wakeupReflection($reflService);
702 }
703
704 protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
705 {
706 $class->initializeReflection($reflService);
707 }
708
709 protected function getDriver(): MappingDriver
710 {
711 assert($this->driver !== null);
712
713 return $this->driver;
714 }
715
716 protected function isEntity(ClassMetadataInterface $class): bool
717 {
718 return ! $class->isMappedSuperclass;
719 }
720
721 private function getTargetPlatform(): Platforms\AbstractPlatform
722 {
723 if (! $this->targetPlatform) {
724 $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
725 }
726
727 return $this->targetPlatform;
728 }
729}
diff --git a/vendor/doctrine/orm/src/Mapping/Column.php b/vendor/doctrine/orm/src/Mapping/Column.php
new file mode 100644
index 0000000..68121e6
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Column.php
@@ -0,0 +1,36 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8use BackedEnum;
9
10#[Attribute(Attribute::TARGET_PROPERTY)]
11final class Column implements MappingAttribute
12{
13 /**
14 * @param int|null $precision The precision for a decimal (exact numeric) column (Applies only for decimal column).
15 * @param int|null $scale The scale for a decimal (exact numeric) column (Applies only for decimal column).
16 * @param class-string<BackedEnum>|null $enumType
17 * @param array<string,mixed> $options
18 * @psalm-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated
19 */
20 public function __construct(
21 public readonly string|null $name = null,
22 public readonly string|null $type = null,
23 public readonly int|null $length = null,
24 public readonly int|null $precision = null,
25 public readonly int|null $scale = null,
26 public readonly bool $unique = false,
27 public readonly bool $nullable = false,
28 public readonly bool $insertable = true,
29 public readonly bool $updatable = true,
30 public readonly string|null $enumType = null,
31 public readonly array $options = [],
32 public readonly string|null $columnDefinition = null,
33 public readonly string|null $generated = null,
34 ) {
35 }
36}
diff --git a/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php b/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php
new file mode 100644
index 0000000..7b31dc3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class CustomIdGenerator implements MappingAttribute
11{
12 public function __construct(
13 public readonly string|null $class = null,
14 ) {
15 }
16}
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php b/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php
new file mode 100644
index 0000000..0b3e7a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use function trim;
8
9/**
10 * The default DefaultEntityListener
11 */
12class DefaultEntityListenerResolver implements EntityListenerResolver
13{
14 /** @psalm-var array<class-string, object> Map to store entity listener instances. */
15 private array $instances = [];
16
17 public function clear(string|null $className = null): void
18 {
19 if ($className === null) {
20 $this->instances = [];
21
22 return;
23 }
24
25 $className = trim($className, '\\');
26 unset($this->instances[$className]);
27 }
28
29 public function register(object $object): void
30 {
31 $this->instances[$object::class] = $object;
32 }
33
34 public function resolve(string $className): object
35 {
36 $className = trim($className, '\\');
37
38 return $this->instances[$className] ??= new $className();
39 }
40}
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php b/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php
new file mode 100644
index 0000000..15218f9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php
@@ -0,0 +1,68 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use function str_contains;
8use function strrpos;
9use function strtolower;
10use function substr;
11
12/**
13 * The default NamingStrategy
14 *
15 * @link www.doctrine-project.org
16 */
17class DefaultNamingStrategy implements NamingStrategy
18{
19 public function classToTableName(string $className): string
20 {
21 if (str_contains($className, '\\')) {
22 return substr($className, strrpos($className, '\\') + 1);
23 }
24
25 return $className;
26 }
27
28 public function propertyToColumnName(string $propertyName, string $className): string
29 {
30 return $propertyName;
31 }
32
33 public function embeddedFieldToColumnName(
34 string $propertyName,
35 string $embeddedColumnName,
36 string $className,
37 string $embeddedClassName,
38 ): string {
39 return $propertyName . '_' . $embeddedColumnName;
40 }
41
42 public function referenceColumnName(): string
43 {
44 return 'id';
45 }
46
47 public function joinColumnName(string $propertyName, string $className): string
48 {
49 return $propertyName . '_' . $this->referenceColumnName();
50 }
51
52 public function joinTableName(
53 string $sourceEntity,
54 string $targetEntity,
55 string $propertyName,
56 ): string {
57 return strtolower($this->classToTableName($sourceEntity) . '_' .
58 $this->classToTableName($targetEntity));
59 }
60
61 public function joinKeyColumnName(
62 string $entityName,
63 string|null $referencedColumnName,
64 ): string {
65 return strtolower($this->classToTableName($entityName) . '_' .
66 ($referencedColumnName ?: $this->referenceColumnName()));
67 }
68}
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php
new file mode 100644
index 0000000..6260336
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php
@@ -0,0 +1,145 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\ORM\Internal\SQLResultCasing;
9
10use function array_map;
11use function array_merge;
12use function assert;
13use function is_numeric;
14use function preg_replace;
15use function substr;
16
17/**
18 * A set of rules for determining the physical column, alias and table quotes
19 */
20class DefaultQuoteStrategy implements QuoteStrategy
21{
22 use SQLResultCasing;
23
24 public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string
25 {
26 return isset($class->fieldMappings[$fieldName]->quoted)
27 ? $platform->quoteIdentifier($class->fieldMappings[$fieldName]->columnName)
28 : $class->fieldMappings[$fieldName]->columnName;
29 }
30
31 /**
32 * {@inheritDoc}
33 *
34 * @todo Table names should be computed in DBAL depending on the platform
35 */
36 public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
37 {
38 $tableName = $class->table['name'];
39
40 if (! empty($class->table['schema'])) {
41 $tableName = $class->table['schema'] . '.' . $class->table['name'];
42 }
43
44 return isset($class->table['quoted'])
45 ? $platform->quoteIdentifier($tableName)
46 : $tableName;
47 }
48
49 /**
50 * {@inheritDoc}
51 */
52 public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
53 {
54 return isset($definition['quoted'])
55 ? $platform->quoteIdentifier($definition['sequenceName'])
56 : $definition['sequenceName'];
57 }
58
59 public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
60 {
61 return isset($joinColumn->quoted)
62 ? $platform->quoteIdentifier($joinColumn->name)
63 : $joinColumn->name;
64 }
65
66 public function getReferencedJoinColumnName(
67 JoinColumnMapping $joinColumn,
68 ClassMetadata $class,
69 AbstractPlatform $platform,
70 ): string {
71 return isset($joinColumn->quoted)
72 ? $platform->quoteIdentifier($joinColumn->referencedColumnName)
73 : $joinColumn->referencedColumnName;
74 }
75
76 public function getJoinTableName(
77 ManyToManyOwningSideMapping $association,
78 ClassMetadata $class,
79 AbstractPlatform $platform,
80 ): string {
81 $schema = '';
82
83 if (isset($association->joinTable->schema)) {
84 $schema = $association->joinTable->schema . '.';
85 }
86
87 $tableName = $association->joinTable->name;
88
89 if (isset($association->joinTable->quoted)) {
90 $tableName = $platform->quoteIdentifier($tableName);
91 }
92
93 return $schema . $tableName;
94 }
95
96 /**
97 * {@inheritDoc}
98 */
99 public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
100 {
101 $quotedColumnNames = [];
102
103 foreach ($class->identifier as $fieldName) {
104 if (isset($class->fieldMappings[$fieldName])) {
105 $quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
106
107 continue;
108 }
109
110 // Association defined as Id field
111 $assoc = $class->associationMappings[$fieldName];
112 assert($assoc->isToOneOwningSide());
113 $joinColumns = $assoc->joinColumns;
114 $assocQuotedColumnNames = array_map(
115 static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted)
116 ? $platform->quoteIdentifier($joinColumn->name)
117 : $joinColumn->name,
118 $joinColumns,
119 );
120
121 $quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames);
122 }
123
124 return $quotedColumnNames;
125 }
126
127 public function getColumnAlias(
128 string $columnName,
129 int $counter,
130 AbstractPlatform $platform,
131 ClassMetadata|null $class = null,
132 ): string {
133 // 1 ) Concatenate column name and counter
134 // 2 ) Trim the column alias to the maximum identifier length of the platform.
135 // If the alias is to long, characters are cut off from the beginning.
136 // 3 ) Strip non alphanumeric characters
137 // 4 ) Prefix with "_" if the result its numeric
138 $columnName .= '_' . $counter;
139 $columnName = substr($columnName, -$platform->getMaxIdentifierLength());
140 $columnName = preg_replace('/[^A-Za-z0-9_]/', '', $columnName);
141 $columnName = is_numeric($columnName) ? '_' . $columnName : $columnName;
142
143 return $this->getSQLResultCasing($platform, $columnName);
144 }
145}
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php
new file mode 100644
index 0000000..49144b8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php
@@ -0,0 +1,80 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use BackedEnum;
8use DateInterval;
9use DateTime;
10use DateTimeImmutable;
11use Doctrine\DBAL\Types\Type;
12use Doctrine\DBAL\Types\Types;
13use ReflectionEnum;
14use ReflectionNamedType;
15use ReflectionProperty;
16
17use function array_merge;
18use function assert;
19use function enum_exists;
20use function is_a;
21
22/** @psalm-type ScalarName = 'array'|'bool'|'float'|'int'|'string' */
23final class DefaultTypedFieldMapper implements TypedFieldMapper
24{
25 /** @var array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
26 private array $typedFieldMappings;
27
28 private const DEFAULT_TYPED_FIELD_MAPPINGS = [
29 DateInterval::class => Types::DATEINTERVAL,
30 DateTime::class => Types::DATETIME_MUTABLE,
31 DateTimeImmutable::class => Types::DATETIME_IMMUTABLE,
32 'array' => Types::JSON,
33 'bool' => Types::BOOLEAN,
34 'float' => Types::FLOAT,
35 'int' => Types::INTEGER,
36 'string' => Types::STRING,
37 ];
38
39 /** @param array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
40 public function __construct(array $typedFieldMappings = [])
41 {
42 $this->typedFieldMappings = array_merge(self::DEFAULT_TYPED_FIELD_MAPPINGS, $typedFieldMappings);
43 }
44
45 /**
46 * {@inheritDoc}
47 */
48 public function validateAndComplete(array $mapping, ReflectionProperty $field): array
49 {
50 $type = $field->getType();
51
52 if (
53 ! isset($mapping['type'])
54 && ($type instanceof ReflectionNamedType)
55 ) {
56 if (! $type->isBuiltin() && enum_exists($type->getName())) {
57 $reflection = new ReflectionEnum($type->getName());
58 if (! $reflection->isBacked()) {
59 throw MappingException::backedEnumTypeRequired(
60 $field->class,
61 $mapping['fieldName'],
62 $type->getName(),
63 );
64 }
65
66 assert(is_a($type->getName(), BackedEnum::class, true));
67 $mapping['enumType'] = $type->getName();
68 $type = $reflection->getBackingType();
69
70 assert($type instanceof ReflectionNamedType);
71 }
72
73 if (isset($this->typedFieldMappings[$type->getName()])) {
74 $mapping['type'] = $this->typedFieldMappings[$type->getName()];
75 }
76 }
77
78 return $mapping;
79 }
80}
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php
new file mode 100644
index 0000000..fb9c7d3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8use BackedEnum;
9
10#[Attribute(Attribute::TARGET_CLASS)]
11final class DiscriminatorColumn implements MappingAttribute
12{
13 public function __construct(
14 public readonly string|null $name = null,
15 public readonly string|null $type = null,
16 public readonly int|null $length = null,
17 public readonly string|null $columnDefinition = null,
18 /** @var class-string<BackedEnum>|null */
19 public readonly string|null $enumType = null,
20 /** @var array<string, mixed> */
21 public readonly array $options = [],
22 ) {
23 }
24}
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php
new file mode 100644
index 0000000..4ccb71c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php
@@ -0,0 +1,83 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8use BackedEnum;
9use Exception;
10
11use function in_array;
12use function property_exists;
13
14/** @template-implements ArrayAccess<string, mixed> */
15final class DiscriminatorColumnMapping implements ArrayAccess
16{
17 use ArrayAccessImplementation;
18
19 /** The database length of the column. Optional. Default value taken from the type. */
20 public int|null $length = null;
21
22 public string|null $columnDefinition = null;
23
24 /** @var class-string<BackedEnum>|null */
25 public string|null $enumType = null;
26
27 /** @var array<string, mixed> */
28 public array $options = [];
29
30 public function __construct(
31 public string $type,
32 public string $fieldName,
33 public string $name,
34 ) {
35 }
36
37 /**
38 * @psalm-param array{
39 * type: string,
40 * fieldName: string,
41 * name: string,
42 * length?: int|null,
43 * columnDefinition?: string|null,
44 * enumType?: class-string<BackedEnum>|null,
45 * options?: array<string, mixed>|null,
46 * } $mappingArray
47 */
48 public static function fromMappingArray(array $mappingArray): self
49 {
50 $mapping = new self(
51 $mappingArray['type'],
52 $mappingArray['fieldName'],
53 $mappingArray['name'],
54 );
55 foreach ($mappingArray as $key => $value) {
56 if (in_array($key, ['type', 'fieldName', 'name'])) {
57 continue;
58 }
59
60 if (property_exists($mapping, $key)) {
61 $mapping->$key = $value ?? $mapping->$key;
62 } else {
63 throw new Exception('Unknown property ' . $key . ' on class ' . static::class);
64 }
65 }
66
67 return $mapping;
68 }
69
70 /** @return list<string> */
71 public function __sleep(): array
72 {
73 $serialized = ['type', 'fieldName', 'name'];
74
75 foreach (['length', 'columnDefinition', 'enumType', 'options'] as $stringOrArrayKey) {
76 if ($this->$stringOrArrayKey !== null) {
77 $serialized[] = $stringOrArrayKey;
78 }
79 }
80
81 return $serialized;
82 }
83}
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php
new file mode 100644
index 0000000..2b204a9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS)]
10final class DiscriminatorMap implements MappingAttribute
11{
12 /** @param array<int|string, string> $value */
13 public function __construct(
14 public readonly array $value,
15 ) {
16 }
17}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php
new file mode 100644
index 0000000..6fed1a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php
@@ -0,0 +1,768 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\ORM\Events;
8use Doctrine\ORM\Mapping;
9use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Mapping\MappingException;
12use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
13use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
14use Doctrine\Persistence\Mapping\Driver\MappingDriver;
15use InvalidArgumentException;
16use ReflectionClass;
17use ReflectionMethod;
18use ReflectionProperty;
19
20use function assert;
21use function class_exists;
22use function constant;
23use function defined;
24use function sprintf;
25
26class AttributeDriver implements MappingDriver
27{
28 use ColocatedMappingDriver;
29 use ReflectionBasedDriver;
30
31 private const ENTITY_ATTRIBUTE_CLASSES = [
32 Mapping\Entity::class => 1,
33 Mapping\MappedSuperclass::class => 2,
34 ];
35
36 private readonly AttributeReader $reader;
37
38 /**
39 * @param array<string> $paths
40 * @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
41 */
42 public function __construct(array $paths, bool $reportFieldsWhereDeclared = true)
43 {
44 if (! $reportFieldsWhereDeclared) {
45 throw new InvalidArgumentException(sprintf(
46 'The $reportFieldsWhereDeclared argument is no longer supported, make sure to omit it when calling %s.',
47 __METHOD__,
48 ));
49 }
50
51 $this->reader = new AttributeReader();
52 $this->addPaths($paths);
53 }
54
55 public function isTransient(string $className): bool
56 {
57 $classAttributes = $this->reader->getClassAttributes(new ReflectionClass($className));
58
59 foreach ($classAttributes as $a) {
60 $attr = $a instanceof RepeatableAttributeCollection ? $a[0] : $a;
61 if (isset(self::ENTITY_ATTRIBUTE_CLASSES[$attr::class])) {
62 return false;
63 }
64 }
65
66 return true;
67 }
68
69 /**
70 * {@inheritDoc}
71 *
72 * @psalm-param class-string<T> $className
73 * @psalm-param ClassMetadata<T> $metadata
74 *
75 * @template T of object
76 */
77 public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void
78 {
79 $reflectionClass = $metadata->getReflectionClass()
80 // this happens when running attribute driver in combination with
81 // static reflection services. This is not the nicest fix
82 ?? new ReflectionClass($metadata->name);
83
84 $classAttributes = $this->reader->getClassAttributes($reflectionClass);
85
86 // Evaluate Entity attribute
87 if (isset($classAttributes[Mapping\Entity::class])) {
88 $entityAttribute = $classAttributes[Mapping\Entity::class];
89 if ($entityAttribute->repositoryClass !== null) {
90 $metadata->setCustomRepositoryClass($entityAttribute->repositoryClass);
91 }
92
93 if ($entityAttribute->readOnly) {
94 $metadata->markReadOnly();
95 }
96 } elseif (isset($classAttributes[Mapping\MappedSuperclass::class])) {
97 $mappedSuperclassAttribute = $classAttributes[Mapping\MappedSuperclass::class];
98
99 $metadata->setCustomRepositoryClass($mappedSuperclassAttribute->repositoryClass);
100 $metadata->isMappedSuperclass = true;
101 } elseif (isset($classAttributes[Mapping\Embeddable::class])) {
102 $metadata->isEmbeddedClass = true;
103 } else {
104 throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
105 }
106
107 $primaryTable = [];
108
109 if (isset($classAttributes[Mapping\Table::class])) {
110 $tableAnnot = $classAttributes[Mapping\Table::class];
111 $primaryTable['name'] = $tableAnnot->name;
112 $primaryTable['schema'] = $tableAnnot->schema;
113
114 if ($tableAnnot->options) {
115 $primaryTable['options'] = $tableAnnot->options;
116 }
117 }
118
119 if (isset($classAttributes[Mapping\Index::class])) {
120 if ($metadata->isEmbeddedClass) {
121 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Index::class);
122 }
123
124 foreach ($classAttributes[Mapping\Index::class] as $idx => $indexAnnot) {
125 $index = [];
126
127 if (! empty($indexAnnot->columns)) {
128 $index['columns'] = $indexAnnot->columns;
129 }
130
131 if (! empty($indexAnnot->fields)) {
132 $index['fields'] = $indexAnnot->fields;
133 }
134
135 if (
136 isset($index['columns'], $index['fields'])
137 || (
138 ! isset($index['columns'])
139 && ! isset($index['fields'])
140 )
141 ) {
142 throw MappingException::invalidIndexConfiguration(
143 $className,
144 (string) ($indexAnnot->name ?? $idx),
145 );
146 }
147
148 if (! empty($indexAnnot->flags)) {
149 $index['flags'] = $indexAnnot->flags;
150 }
151
152 if (! empty($indexAnnot->options)) {
153 $index['options'] = $indexAnnot->options;
154 }
155
156 if (! empty($indexAnnot->name)) {
157 $primaryTable['indexes'][$indexAnnot->name] = $index;
158 } else {
159 $primaryTable['indexes'][] = $index;
160 }
161 }
162 }
163
164 if (isset($classAttributes[Mapping\UniqueConstraint::class])) {
165 if ($metadata->isEmbeddedClass) {
166 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\UniqueConstraint::class);
167 }
168
169 foreach ($classAttributes[Mapping\UniqueConstraint::class] as $idx => $uniqueConstraintAnnot) {
170 $uniqueConstraint = [];
171
172 if (! empty($uniqueConstraintAnnot->columns)) {
173 $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns;
174 }
175
176 if (! empty($uniqueConstraintAnnot->fields)) {
177 $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields;
178 }
179
180 if (
181 isset($uniqueConstraint['columns'], $uniqueConstraint['fields'])
182 || (
183 ! isset($uniqueConstraint['columns'])
184 && ! isset($uniqueConstraint['fields'])
185 )
186 ) {
187 throw MappingException::invalidUniqueConstraintConfiguration(
188 $className,
189 (string) ($uniqueConstraintAnnot->name ?? $idx),
190 );
191 }
192
193 if (! empty($uniqueConstraintAnnot->options)) {
194 $uniqueConstraint['options'] = $uniqueConstraintAnnot->options;
195 }
196
197 if (! empty($uniqueConstraintAnnot->name)) {
198 $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint;
199 } else {
200 $primaryTable['uniqueConstraints'][] = $uniqueConstraint;
201 }
202 }
203 }
204
205 $metadata->setPrimaryTable($primaryTable);
206
207 // Evaluate #[Cache] attribute
208 if (isset($classAttributes[Mapping\Cache::class])) {
209 if ($metadata->isEmbeddedClass) {
210 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Cache::class);
211 }
212
213 $cacheAttribute = $classAttributes[Mapping\Cache::class];
214 $cacheMap = [
215 'region' => $cacheAttribute->region,
216 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage),
217 ];
218
219 $metadata->enableCache($cacheMap);
220 }
221
222 // Evaluate InheritanceType attribute
223 if (isset($classAttributes[Mapping\InheritanceType::class])) {
224 if ($metadata->isEmbeddedClass) {
225 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\InheritanceType::class);
226 }
227
228 $inheritanceTypeAttribute = $classAttributes[Mapping\InheritanceType::class];
229
230 $metadata->setInheritanceType(
231 constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAttribute->value),
232 );
233
234 if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
235 // Evaluate DiscriminatorColumn attribute
236 if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) {
237 $discrColumnAttribute = $classAttributes[Mapping\DiscriminatorColumn::class];
238 assert($discrColumnAttribute instanceof Mapping\DiscriminatorColumn);
239
240 $columnDef = [
241 'name' => $discrColumnAttribute->name,
242 'type' => $discrColumnAttribute->type ?? 'string',
243 'length' => $discrColumnAttribute->length ?? 255,
244 'columnDefinition' => $discrColumnAttribute->columnDefinition,
245 'enumType' => $discrColumnAttribute->enumType,
246 ];
247
248 if ($discrColumnAttribute->options) {
249 $columnDef['options'] = $discrColumnAttribute->options;
250 }
251
252 $metadata->setDiscriminatorColumn($columnDef);
253 } else {
254 $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]);
255 }
256
257 // Evaluate DiscriminatorMap attribute
258 if (isset($classAttributes[Mapping\DiscriminatorMap::class])) {
259 $discrMapAttribute = $classAttributes[Mapping\DiscriminatorMap::class];
260 $metadata->setDiscriminatorMap($discrMapAttribute->value);
261 }
262 }
263 }
264
265 // Evaluate DoctrineChangeTrackingPolicy attribute
266 if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) {
267 if ($metadata->isEmbeddedClass) {
268 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ChangeTrackingPolicy::class);
269 }
270
271 $changeTrackingAttribute = $classAttributes[Mapping\ChangeTrackingPolicy::class];
272 $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAttribute->value));
273 }
274
275 foreach ($reflectionClass->getProperties() as $property) {
276 assert($property instanceof ReflectionProperty);
277
278 if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
279 continue;
280 }
281
282 $mapping = [];
283 $mapping['fieldName'] = $property->name;
284
285 // Evaluate #[Cache] attribute
286 $cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class);
287 if ($cacheAttribute !== null) {
288 assert($cacheAttribute instanceof Mapping\Cache);
289
290 $mapping['cache'] = $metadata->getAssociationCacheDefaults(
291 $mapping['fieldName'],
292 [
293 'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage),
294 'region' => $cacheAttribute->region,
295 ],
296 );
297 }
298
299 // Check for JoinColumn/JoinColumns attributes
300 $joinColumns = [];
301
302 $joinColumnAttributes = $this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class);
303
304 foreach ($joinColumnAttributes as $joinColumnAttribute) {
305 $joinColumns[] = $this->joinColumnToArray($joinColumnAttribute);
306 }
307
308 // Field can only be attributed with one of:
309 // Column, OneToOne, OneToMany, ManyToOne, ManyToMany, Embedded
310 $columnAttribute = $this->reader->getPropertyAttribute($property, Mapping\Column::class);
311 $oneToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToOne::class);
312 $oneToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToMany::class);
313 $manyToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToOne::class);
314 $manyToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToMany::class);
315 $embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class);
316
317 if ($columnAttribute !== null) {
318 $mapping = $this->columnToArray($property->name, $columnAttribute);
319
320 if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
321 $mapping['id'] = true;
322 }
323
324 $generatedValueAttribute = $this->reader->getPropertyAttribute($property, Mapping\GeneratedValue::class);
325
326 if ($generatedValueAttribute !== null) {
327 $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAttribute->strategy));
328 }
329
330 if ($this->reader->getPropertyAttribute($property, Mapping\Version::class)) {
331 $metadata->setVersionMapping($mapping);
332 }
333
334 $metadata->mapField($mapping);
335
336 // Check for SequenceGenerator/TableGenerator definition
337 $seqGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\SequenceGenerator::class);
338 $customGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\CustomIdGenerator::class);
339
340 if ($seqGeneratorAttribute !== null) {
341 $metadata->setSequenceGeneratorDefinition(
342 [
343 'sequenceName' => $seqGeneratorAttribute->sequenceName,
344 'allocationSize' => $seqGeneratorAttribute->allocationSize,
345 'initialValue' => $seqGeneratorAttribute->initialValue,
346 ],
347 );
348 } elseif ($customGeneratorAttribute !== null) {
349 $metadata->setCustomGeneratorDefinition(
350 [
351 'class' => $customGeneratorAttribute->class,
352 ],
353 );
354 }
355 } elseif ($oneToOneAttribute !== null) {
356 if ($metadata->isEmbeddedClass) {
357 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToOne::class);
358 }
359
360 if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
361 $mapping['id'] = true;
362 }
363
364 $mapping['targetEntity'] = $oneToOneAttribute->targetEntity;
365 $mapping['joinColumns'] = $joinColumns;
366 $mapping['mappedBy'] = $oneToOneAttribute->mappedBy;
367 $mapping['inversedBy'] = $oneToOneAttribute->inversedBy;
368 $mapping['cascade'] = $oneToOneAttribute->cascade;
369 $mapping['orphanRemoval'] = $oneToOneAttribute->orphanRemoval;
370 $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAttribute->fetch);
371 $metadata->mapOneToOne($mapping);
372 } elseif ($oneToManyAttribute !== null) {
373 if ($metadata->isEmbeddedClass) {
374 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
375 }
376
377 $mapping['mappedBy'] = $oneToManyAttribute->mappedBy;
378 $mapping['targetEntity'] = $oneToManyAttribute->targetEntity;
379 $mapping['cascade'] = $oneToManyAttribute->cascade;
380 $mapping['indexBy'] = $oneToManyAttribute->indexBy;
381 $mapping['orphanRemoval'] = $oneToManyAttribute->orphanRemoval;
382 $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAttribute->fetch);
383
384 $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class);
385
386 if ($orderByAttribute !== null) {
387 $mapping['orderBy'] = $orderByAttribute->value;
388 }
389
390 $metadata->mapOneToMany($mapping);
391 } elseif ($manyToOneAttribute !== null) {
392 if ($metadata->isEmbeddedClass) {
393 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
394 }
395
396 $idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);
397
398 if ($idAttribute !== null) {
399 $mapping['id'] = true;
400 }
401
402 $mapping['joinColumns'] = $joinColumns;
403 $mapping['cascade'] = $manyToOneAttribute->cascade;
404 $mapping['inversedBy'] = $manyToOneAttribute->inversedBy;
405 $mapping['targetEntity'] = $manyToOneAttribute->targetEntity;
406 $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAttribute->fetch);
407 $metadata->mapManyToOne($mapping);
408 } elseif ($manyToManyAttribute !== null) {
409 if ($metadata->isEmbeddedClass) {
410 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToMany::class);
411 }
412
413 $joinTable = [];
414 $joinTableAttribute = $this->reader->getPropertyAttribute($property, Mapping\JoinTable::class);
415
416 if ($joinTableAttribute !== null) {
417 $joinTable = [
418 'name' => $joinTableAttribute->name,
419 'schema' => $joinTableAttribute->schema,
420 ];
421
422 if ($joinTableAttribute->options) {
423 $joinTable['options'] = $joinTableAttribute->options;
424 }
425
426 foreach ($joinTableAttribute->joinColumns as $joinColumn) {
427 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
428 }
429
430 foreach ($joinTableAttribute->inverseJoinColumns as $joinColumn) {
431 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
432 }
433 }
434
435 foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class) as $joinColumn) {
436 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
437 }
438
439 foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) {
440 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
441 }
442
443 $mapping['joinTable'] = $joinTable;
444 $mapping['targetEntity'] = $manyToManyAttribute->targetEntity;
445 $mapping['mappedBy'] = $manyToManyAttribute->mappedBy;
446 $mapping['inversedBy'] = $manyToManyAttribute->inversedBy;
447 $mapping['cascade'] = $manyToManyAttribute->cascade;
448 $mapping['indexBy'] = $manyToManyAttribute->indexBy;
449 $mapping['orphanRemoval'] = $manyToManyAttribute->orphanRemoval;
450 $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAttribute->fetch);
451
452 $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class);
453
454 if ($orderByAttribute !== null) {
455 $mapping['orderBy'] = $orderByAttribute->value;
456 }
457
458 $metadata->mapManyToMany($mapping);
459 } elseif ($embeddedAttribute !== null) {
460 $mapping['class'] = $embeddedAttribute->class;
461 $mapping['columnPrefix'] = $embeddedAttribute->columnPrefix;
462
463 $metadata->mapEmbedded($mapping);
464 }
465 }
466
467 // Evaluate AssociationOverrides attribute
468 if (isset($classAttributes[Mapping\AssociationOverrides::class])) {
469 if ($metadata->isEmbeddedClass) {
470 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AssociationOverride::class);
471 }
472
473 $associationOverride = $classAttributes[Mapping\AssociationOverrides::class];
474
475 foreach ($associationOverride->overrides as $associationOverride) {
476 $override = [];
477 $fieldName = $associationOverride->name;
478
479 // Check for JoinColumn/JoinColumns attributes
480 if ($associationOverride->joinColumns) {
481 $joinColumns = [];
482
483 foreach ($associationOverride->joinColumns as $joinColumn) {
484 $joinColumns[] = $this->joinColumnToArray($joinColumn);
485 }
486
487 $override['joinColumns'] = $joinColumns;
488 }
489
490 if ($associationOverride->inverseJoinColumns) {
491 $joinColumns = [];
492
493 foreach ($associationOverride->inverseJoinColumns as $joinColumn) {
494 $joinColumns[] = $this->joinColumnToArray($joinColumn);
495 }
496
497 $override['inverseJoinColumns'] = $joinColumns;
498 }
499
500 // Check for JoinTable attributes
501 if ($associationOverride->joinTable) {
502 $joinTableAnnot = $associationOverride->joinTable;
503 $joinTable = [
504 'name' => $joinTableAnnot->name,
505 'schema' => $joinTableAnnot->schema,
506 'joinColumns' => $override['joinColumns'] ?? [],
507 'inverseJoinColumns' => $override['inverseJoinColumns'] ?? [],
508 ];
509
510 unset($override['joinColumns'], $override['inverseJoinColumns']);
511
512 $override['joinTable'] = $joinTable;
513 }
514
515 // Check for inversedBy
516 if ($associationOverride->inversedBy) {
517 $override['inversedBy'] = $associationOverride->inversedBy;
518 }
519
520 // Check for `fetch`
521 if ($associationOverride->fetch) {
522 $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . $associationOverride->fetch);
523 }
524
525 $metadata->setAssociationOverride($fieldName, $override);
526 }
527 }
528
529 // Evaluate AttributeOverrides attribute
530 if (isset($classAttributes[Mapping\AttributeOverrides::class])) {
531 if ($metadata->isEmbeddedClass) {
532 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AttributeOverrides::class);
533 }
534
535 $attributeOverridesAnnot = $classAttributes[Mapping\AttributeOverrides::class];
536
537 foreach ($attributeOverridesAnnot->overrides as $attributeOverride) {
538 $mapping = $this->columnToArray($attributeOverride->name, $attributeOverride->column);
539
540 $metadata->setAttributeOverride($attributeOverride->name, $mapping);
541 }
542 }
543
544 // Evaluate EntityListeners attribute
545 if (isset($classAttributes[Mapping\EntityListeners::class])) {
546 if ($metadata->isEmbeddedClass) {
547 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\EntityListeners::class);
548 }
549
550 $entityListenersAttribute = $classAttributes[Mapping\EntityListeners::class];
551
552 foreach ($entityListenersAttribute->value as $item) {
553 $listenerClassName = $metadata->fullyQualifiedClassName($item);
554
555 if (! class_exists($listenerClassName)) {
556 throw MappingException::entityListenerClassNotFound($listenerClassName, $className);
557 }
558
559 $hasMapping = false;
560 $listenerClass = new ReflectionClass($listenerClassName);
561
562 foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
563 assert($method instanceof ReflectionMethod);
564 // find method callbacks.
565 $callbacks = $this->getMethodCallbacks($method);
566 $hasMapping = $hasMapping ?: ! empty($callbacks);
567
568 foreach ($callbacks as $value) {
569 $metadata->addEntityListener($value[1], $listenerClassName, $value[0]);
570 }
571 }
572
573 // Evaluate the listener using naming convention.
574 if (! $hasMapping) {
575 EntityListenerBuilder::bindEntityListener($metadata, $listenerClassName);
576 }
577 }
578 }
579
580 // Evaluate #[HasLifecycleCallbacks] attribute
581 if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) {
582 if ($metadata->isEmbeddedClass) {
583 throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\HasLifecycleCallbacks::class);
584 }
585
586 foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
587 assert($method instanceof ReflectionMethod);
588 foreach ($this->getMethodCallbacks($method) as $value) {
589 $metadata->addLifecycleCallback($value[0], $value[1]);
590 }
591 }
592 }
593 }
594
595 /**
596 * Attempts to resolve the fetch mode.
597 *
598 * @param class-string $className The class name.
599 * @param string $fetchMode The fetch mode.
600 *
601 * @return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata.
602 *
603 * @throws MappingException If the fetch mode is not valid.
604 */
605 private function getFetchMode(string $className, string $fetchMode): int
606 {
607 if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) {
608 throw MappingException::invalidFetchMode($className, $fetchMode);
609 }
610
611 return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode);
612 }
613
614 /**
615 * Attempts to resolve the generated mode.
616 *
617 * @throws MappingException If the fetch mode is not valid.
618 */
619 private function getGeneratedMode(string $generatedMode): int
620 {
621 if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) {
622 throw MappingException::invalidGeneratedMode($generatedMode);
623 }
624
625 return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode);
626 }
627
628 /**
629 * Parses the given method.
630 *
631 * @return list<array{string, string}>
632 * @psalm-return list<array{string, (Events::*)}>
633 */
634 private function getMethodCallbacks(ReflectionMethod $method): array
635 {
636 $callbacks = [];
637 $attributes = $this->reader->getMethodAttributes($method);
638
639 foreach ($attributes as $attribute) {
640 if ($attribute instanceof Mapping\PrePersist) {
641 $callbacks[] = [$method->name, Events::prePersist];
642 }
643
644 if ($attribute instanceof Mapping\PostPersist) {
645 $callbacks[] = [$method->name, Events::postPersist];
646 }
647
648 if ($attribute instanceof Mapping\PreUpdate) {
649 $callbacks[] = [$method->name, Events::preUpdate];
650 }
651
652 if ($attribute instanceof Mapping\PostUpdate) {
653 $callbacks[] = [$method->name, Events::postUpdate];
654 }
655
656 if ($attribute instanceof Mapping\PreRemove) {
657 $callbacks[] = [$method->name, Events::preRemove];
658 }
659
660 if ($attribute instanceof Mapping\PostRemove) {
661 $callbacks[] = [$method->name, Events::postRemove];
662 }
663
664 if ($attribute instanceof Mapping\PostLoad) {
665 $callbacks[] = [$method->name, Events::postLoad];
666 }
667
668 if ($attribute instanceof Mapping\PreFlush) {
669 $callbacks[] = [$method->name, Events::preFlush];
670 }
671 }
672
673 return $callbacks;
674 }
675
676 /**
677 * Parse the given JoinColumn as array
678 *
679 * @return mixed[]
680 * @psalm-return array{
681 * name: string|null,
682 * unique: bool,
683 * nullable: bool,
684 * onDelete: mixed,
685 * columnDefinition: string|null,
686 * referencedColumnName: string,
687 * options?: array<string, mixed>
688 * }
689 */
690 private function joinColumnToArray(Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn): array
691 {
692 $mapping = [
693 'name' => $joinColumn->name,
694 'unique' => $joinColumn->unique,
695 'nullable' => $joinColumn->nullable,
696 'onDelete' => $joinColumn->onDelete,
697 'columnDefinition' => $joinColumn->columnDefinition,
698 'referencedColumnName' => $joinColumn->referencedColumnName,
699 ];
700
701 if ($joinColumn->options) {
702 $mapping['options'] = $joinColumn->options;
703 }
704
705 return $mapping;
706 }
707
708 /**
709 * Parse the given Column as array
710 *
711 * @return mixed[]
712 * @psalm-return array{
713 * fieldName: string,
714 * type: mixed,
715 * scale: int,
716 * length: int,
717 * unique: bool,
718 * nullable: bool,
719 * precision: int,
720 * enumType?: class-string,
721 * options?: mixed[],
722 * columnName?: string,
723 * columnDefinition?: string
724 * }
725 */
726 private function columnToArray(string $fieldName, Mapping\Column $column): array
727 {
728 $mapping = [
729 'fieldName' => $fieldName,
730 'type' => $column->type,
731 'scale' => $column->scale,
732 'length' => $column->length,
733 'unique' => $column->unique,
734 'nullable' => $column->nullable,
735 'precision' => $column->precision,
736 ];
737
738 if ($column->options) {
739 $mapping['options'] = $column->options;
740 }
741
742 if (isset($column->name)) {
743 $mapping['columnName'] = $column->name;
744 }
745
746 if (isset($column->columnDefinition)) {
747 $mapping['columnDefinition'] = $column->columnDefinition;
748 }
749
750 if ($column->updatable === false) {
751 $mapping['notUpdatable'] = true;
752 }
753
754 if ($column->insertable === false) {
755 $mapping['notInsertable'] = true;
756 }
757
758 if ($column->generated !== null) {
759 $mapping['generated'] = $this->getGeneratedMode($column->generated);
760 }
761
762 if ($column->enumType) {
763 $mapping['enumType'] = $column->enumType;
764 }
765
766 return $mapping;
767 }
768}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php b/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php
new file mode 100644
index 0000000..2de622a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php
@@ -0,0 +1,146 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Attribute;
8use Doctrine\ORM\Mapping\MappingAttribute;
9use LogicException;
10use ReflectionAttribute;
11use ReflectionClass;
12use ReflectionMethod;
13use ReflectionProperty;
14
15use function assert;
16use function is_string;
17use function is_subclass_of;
18use function sprintf;
19
20/** @internal */
21final class AttributeReader
22{
23 /** @var array<class-string<MappingAttribute>, bool> */
24 private array $isRepeatableAttribute = [];
25
26 /**
27 * @psalm-return class-string-map<T, T|RepeatableAttributeCollection<T>>
28 *
29 * @template T of MappingAttribute
30 */
31 public function getClassAttributes(ReflectionClass $class): array
32 {
33 return $this->convertToAttributeInstances($class->getAttributes());
34 }
35
36 /**
37 * @return class-string-map<T, T|RepeatableAttributeCollection<T>>
38 *
39 * @template T of MappingAttribute
40 */
41 public function getMethodAttributes(ReflectionMethod $method): array
42 {
43 return $this->convertToAttributeInstances($method->getAttributes());
44 }
45
46 /**
47 * @return class-string-map<T, T|RepeatableAttributeCollection<T>>
48 *
49 * @template T of MappingAttribute
50 */
51 public function getPropertyAttributes(ReflectionProperty $property): array
52 {
53 return $this->convertToAttributeInstances($property->getAttributes());
54 }
55
56 /**
57 * @param class-string<T> $attributeName The name of the annotation.
58 *
59 * @return T|null
60 *
61 * @template T of MappingAttribute
62 */
63 public function getPropertyAttribute(ReflectionProperty $property, string $attributeName)
64 {
65 if ($this->isRepeatable($attributeName)) {
66 throw new LogicException(sprintf(
67 'The attribute "%s" is repeatable. Call getPropertyAttributeCollection() instead.',
68 $attributeName,
69 ));
70 }
71
72 return $this->getPropertyAttributes($property)[$attributeName] ?? null;
73 }
74
75 /**
76 * @param class-string<T> $attributeName The name of the annotation.
77 *
78 * @return RepeatableAttributeCollection<T>
79 *
80 * @template T of MappingAttribute
81 */
82 public function getPropertyAttributeCollection(
83 ReflectionProperty $property,
84 string $attributeName,
85 ): RepeatableAttributeCollection {
86 if (! $this->isRepeatable($attributeName)) {
87 throw new LogicException(sprintf(
88 'The attribute "%s" is not repeatable. Call getPropertyAttribute() instead.',
89 $attributeName,
90 ));
91 }
92
93 return $this->getPropertyAttributes($property)[$attributeName] ?? new RepeatableAttributeCollection();
94 }
95
96 /**
97 * @param array<ReflectionAttribute> $attributes
98 *
99 * @return class-string-map<T, T|RepeatableAttributeCollection<T>>
100 *
101 * @template T of MappingAttribute
102 */
103 private function convertToAttributeInstances(array $attributes): array
104 {
105 $instances = [];
106
107 foreach ($attributes as $attribute) {
108 $attributeName = $attribute->getName();
109 assert(is_string($attributeName));
110 // Make sure we only get Doctrine Attributes
111 if (! is_subclass_of($attributeName, MappingAttribute::class)) {
112 continue;
113 }
114
115 $instance = $attribute->newInstance();
116 assert($instance instanceof MappingAttribute);
117
118 if ($this->isRepeatable($attributeName)) {
119 if (! isset($instances[$attributeName])) {
120 $instances[$attributeName] = new RepeatableAttributeCollection();
121 }
122
123 $collection = $instances[$attributeName];
124 assert($collection instanceof RepeatableAttributeCollection);
125 $collection[] = $instance;
126 } else {
127 $instances[$attributeName] = $instance;
128 }
129 }
130
131 return $instances;
132 }
133
134 /** @param class-string<MappingAttribute> $attributeClassName */
135 private function isRepeatable(string $attributeClassName): bool
136 {
137 if (isset($this->isRepeatableAttribute[$attributeClassName])) {
138 return $this->isRepeatableAttribute[$attributeClassName];
139 }
140
141 $reflectionClass = new ReflectionClass($attributeClassName);
142 $attribute = $reflectionClass->getAttributes()[0]->newInstance();
143
144 return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0;
145 }
146}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
new file mode 100644
index 0000000..49e2e93
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php
@@ -0,0 +1,528 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\DBAL\Schema\AbstractSchemaManager;
8use Doctrine\DBAL\Schema\Column;
9use Doctrine\DBAL\Schema\SchemaException;
10use Doctrine\DBAL\Schema\Table;
11use Doctrine\DBAL\Types\Type;
12use Doctrine\DBAL\Types\Types;
13use Doctrine\Inflector\Inflector;
14use Doctrine\Inflector\InflectorFactory;
15use Doctrine\ORM\Mapping\ClassMetadata;
16use Doctrine\ORM\Mapping\MappingException;
17use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
18use Doctrine\Persistence\Mapping\Driver\MappingDriver;
19use InvalidArgumentException;
20use TypeError;
21
22use function array_diff;
23use function array_keys;
24use function array_merge;
25use function assert;
26use function count;
27use function current;
28use function get_debug_type;
29use function in_array;
30use function preg_replace;
31use function sort;
32use function sprintf;
33use function strtolower;
34
35/**
36 * The DatabaseDriver reverse engineers the mapping metadata from a database.
37 *
38 * @link www.doctrine-project.org
39 */
40class DatabaseDriver implements MappingDriver
41{
42 /**
43 * Replacement for {@see Types::ARRAY}.
44 *
45 * To be removed as soon as support for DBAL 3 is dropped.
46 */
47 private const ARRAY = 'array';
48
49 /**
50 * Replacement for {@see Types::OBJECT}.
51 *
52 * To be removed as soon as support for DBAL 3 is dropped.
53 */
54 private const OBJECT = 'object';
55
56 /** @var array<string,Table>|null */
57 private array|null $tables = null;
58
59 /** @var array<class-string, string> */
60 private array $classToTableNames = [];
61
62 /** @psalm-var array<string, Table> */
63 private array $manyToManyTables = [];
64
65 /** @var mixed[] */
66 private array $classNamesForTables = [];
67
68 /** @var mixed[] */
69 private array $fieldNamesForColumns = [];
70
71 /**
72 * The namespace for the generated entities.
73 */
74 private string|null $namespace = null;
75
76 private Inflector $inflector;
77
78 public function __construct(private readonly AbstractSchemaManager $sm)
79 {
80 $this->inflector = InflectorFactory::create()->build();
81 }
82
83 /**
84 * Set the namespace for the generated entities.
85 */
86 public function setNamespace(string $namespace): void
87 {
88 $this->namespace = $namespace;
89 }
90
91 public function isTransient(string $className): bool
92 {
93 return true;
94 }
95
96 /**
97 * {@inheritDoc}
98 */
99 public function getAllClassNames(): array
100 {
101 $this->reverseEngineerMappingFromDatabase();
102
103 return array_keys($this->classToTableNames);
104 }
105
106 /**
107 * Sets class name for a table.
108 */
109 public function setClassNameForTable(string $tableName, string $className): void
110 {
111 $this->classNamesForTables[$tableName] = $className;
112 }
113
114 /**
115 * Sets field name for a column on a specific table.
116 */
117 public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void
118 {
119 $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
120 }
121
122 /**
123 * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
124 *
125 * @param Table[] $entityTables
126 * @param Table[] $manyToManyTables
127 * @psalm-param list<Table> $entityTables
128 * @psalm-param list<Table> $manyToManyTables
129 */
130 public function setTables(array $entityTables, array $manyToManyTables): void
131 {
132 $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
133
134 foreach ($entityTables as $table) {
135 $className = $this->getClassNameForTable($table->getName());
136
137 $this->classToTableNames[$className] = $table->getName();
138 $this->tables[$table->getName()] = $table;
139 }
140
141 foreach ($manyToManyTables as $table) {
142 $this->manyToManyTables[$table->getName()] = $table;
143 }
144 }
145
146 public function setInflector(Inflector $inflector): void
147 {
148 $this->inflector = $inflector;
149 }
150
151 /**
152 * {@inheritDoc}
153 *
154 * @psalm-param class-string<T> $className
155 * @psalm-param ClassMetadata<T> $metadata
156 *
157 * @template T of object
158 */
159 public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void
160 {
161 if (! $metadata instanceof ClassMetadata) {
162 throw new TypeError(sprintf(
163 'Argument #2 passed to %s() must be an instance of %s, %s given.',
164 __METHOD__,
165 ClassMetadata::class,
166 get_debug_type($metadata),
167 ));
168 }
169
170 $this->reverseEngineerMappingFromDatabase();
171
172 if (! isset($this->classToTableNames[$className])) {
173 throw new InvalidArgumentException('Unknown class ' . $className);
174 }
175
176 $tableName = $this->classToTableNames[$className];
177
178 $metadata->name = $className;
179 $metadata->table['name'] = $tableName;
180
181 $this->buildIndexes($metadata);
182 $this->buildFieldMappings($metadata);
183 $this->buildToOneAssociationMappings($metadata);
184
185 foreach ($this->manyToManyTables as $manyTable) {
186 foreach ($manyTable->getForeignKeys() as $foreignKey) {
187 // foreign key maps to the table of the current entity, many to many association probably exists
188 if (! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) {
189 continue;
190 }
191
192 $myFk = $foreignKey;
193 $otherFk = null;
194
195 foreach ($manyTable->getForeignKeys() as $foreignKey) {
196 if ($foreignKey !== $myFk) {
197 $otherFk = $foreignKey;
198 break;
199 }
200 }
201
202 if (! $otherFk) {
203 // the definition of this many to many table does not contain
204 // enough foreign key information to continue reverse engineering.
205 continue;
206 }
207
208 $localColumn = current($myFk->getLocalColumns());
209
210 $associationMapping = [];
211 $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getLocalColumns()), true);
212 $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
213
214 if (current($manyTable->getColumns())->getName() === $localColumn) {
215 $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true);
216 $associationMapping['joinTable'] = [
217 'name' => strtolower($manyTable->getName()),
218 'joinColumns' => [],
219 'inverseJoinColumns' => [],
220 ];
221
222 $fkCols = $myFk->getForeignColumns();
223 $cols = $myFk->getLocalColumns();
224
225 for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
226 $associationMapping['joinTable']['joinColumns'][] = [
227 'name' => $cols[$i],
228 'referencedColumnName' => $fkCols[$i],
229 ];
230 }
231
232 $fkCols = $otherFk->getForeignColumns();
233 $cols = $otherFk->getLocalColumns();
234
235 for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
236 $associationMapping['joinTable']['inverseJoinColumns'][] = [
237 'name' => $cols[$i],
238 'referencedColumnName' => $fkCols[$i],
239 ];
240 }
241 } else {
242 $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true);
243 }
244
245 $metadata->mapManyToMany($associationMapping);
246
247 break;
248 }
249 }
250 }
251
252 /** @throws MappingException */
253 private function reverseEngineerMappingFromDatabase(): void
254 {
255 if ($this->tables !== null) {
256 return;
257 }
258
259 $this->tables = $this->manyToManyTables = $this->classToTableNames = [];
260
261 foreach ($this->sm->listTables() as $table) {
262 $tableName = $table->getName();
263 $foreignKeys = $table->getForeignKeys();
264
265 $allForeignKeyColumns = [];
266
267 foreach ($foreignKeys as $foreignKey) {
268 $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
269 }
270
271 $primaryKey = $table->getPrimaryKey();
272 if ($primaryKey === null) {
273 throw new MappingException(
274 'Table ' . $tableName . ' has no primary key. Doctrine does not ' .
275 "support reverse engineering from tables that don't have a primary key.",
276 );
277 }
278
279 $pkColumns = $primaryKey->getColumns();
280
281 sort($pkColumns);
282 sort($allForeignKeyColumns);
283
284 if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {
285 $this->manyToManyTables[$tableName] = $table;
286 } else {
287 // lower-casing is necessary because of Oracle Uppercase Tablenames,
288 // assumption is lower-case + underscore separated.
289 $className = $this->getClassNameForTable($tableName);
290
291 $this->tables[$tableName] = $table;
292 $this->classToTableNames[$className] = $tableName;
293 }
294 }
295 }
296
297 /**
298 * Build indexes from a class metadata.
299 */
300 private function buildIndexes(ClassMetadata $metadata): void
301 {
302 $tableName = $metadata->table['name'];
303 $indexes = $this->tables[$tableName]->getIndexes();
304
305 foreach ($indexes as $index) {
306 if ($index->isPrimary()) {
307 continue;
308 }
309
310 $indexName = $index->getName();
311 $indexColumns = $index->getColumns();
312 $constraintType = $index->isUnique()
313 ? 'uniqueConstraints'
314 : 'indexes';
315
316 $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns;
317 }
318 }
319
320 /**
321 * Build field mapping from class metadata.
322 */
323 private function buildFieldMappings(ClassMetadata $metadata): void
324 {
325 $tableName = $metadata->table['name'];
326 $columns = $this->tables[$tableName]->getColumns();
327 $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
328 $foreignKeys = $this->tables[$tableName]->getForeignKeys();
329 $allForeignKeys = [];
330
331 foreach ($foreignKeys as $foreignKey) {
332 $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
333 }
334
335 $ids = [];
336 $fieldMappings = [];
337
338 foreach ($columns as $column) {
339 if (in_array($column->getName(), $allForeignKeys, true)) {
340 continue;
341 }
342
343 $fieldMapping = $this->buildFieldMapping($tableName, $column);
344
345 if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) {
346 $fieldMapping['id'] = true;
347 $ids[] = $fieldMapping;
348 }
349
350 $fieldMappings[] = $fieldMapping;
351 }
352
353 // We need to check for the columns here, because we might have associations as id as well.
354 if ($ids && count($primaryKeys) === 1) {
355 $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
356 }
357
358 foreach ($fieldMappings as $fieldMapping) {
359 $metadata->mapField($fieldMapping);
360 }
361 }
362
363 /**
364 * Build field mapping from a schema column definition
365 *
366 * @return mixed[]
367 * @psalm-return array{
368 * fieldName: string,
369 * columnName: string,
370 * type: string,
371 * nullable: bool,
372 * options: array{
373 * unsigned?: bool,
374 * fixed?: bool,
375 * comment: string|null,
376 * default?: mixed
377 * },
378 * precision?: int,
379 * scale?: int,
380 * length?: int|null
381 * }
382 */
383 private function buildFieldMapping(string $tableName, Column $column): array
384 {
385 $fieldMapping = [
386 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false),
387 'columnName' => $column->getName(),
388 'type' => Type::getTypeRegistry()->lookupName($column->getType()),
389 'nullable' => ! $column->getNotnull(),
390 'options' => [
391 'comment' => $column->getComment(),
392 ],
393 ];
394
395 // Type specific elements
396 switch ($fieldMapping['type']) {
397 case self::ARRAY:
398 case Types::BLOB:
399 case Types::GUID:
400 case self::OBJECT:
401 case Types::SIMPLE_ARRAY:
402 case Types::STRING:
403 case Types::TEXT:
404 $fieldMapping['length'] = $column->getLength();
405 $fieldMapping['options']['fixed'] = $column->getFixed();
406 break;
407
408 case Types::DECIMAL:
409 case Types::FLOAT:
410 $fieldMapping['precision'] = $column->getPrecision();
411 $fieldMapping['scale'] = $column->getScale();
412 break;
413
414 case Types::INTEGER:
415 case Types::BIGINT:
416 case Types::SMALLINT:
417 $fieldMapping['options']['unsigned'] = $column->getUnsigned();
418 break;
419 }
420
421 // Default
422 $default = $column->getDefault();
423 if ($default !== null) {
424 $fieldMapping['options']['default'] = $default;
425 }
426
427 return $fieldMapping;
428 }
429
430 /**
431 * Build to one (one to one, many to one) association mapping from class metadata.
432 */
433 private function buildToOneAssociationMappings(ClassMetadata $metadata): void
434 {
435 assert($this->tables !== null);
436
437 $tableName = $metadata->table['name'];
438 $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
439 $foreignKeys = $this->tables[$tableName]->getForeignKeys();
440
441 foreach ($foreignKeys as $foreignKey) {
442 $foreignTableName = $foreignKey->getForeignTableName();
443 $fkColumns = $foreignKey->getLocalColumns();
444 $fkForeignColumns = $foreignKey->getForeignColumns();
445 $localColumn = current($fkColumns);
446 $associationMapping = [
447 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true),
448 'targetEntity' => $this->getClassNameForTable($foreignTableName),
449 ];
450
451 if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) {
452 $associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
453 }
454
455 if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) {
456 $associationMapping['id'] = true;
457 }
458
459 for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) {
460 $associationMapping['joinColumns'][] = [
461 'name' => $fkColumns[$i],
462 'referencedColumnName' => $fkForeignColumns[$i],
463 ];
464 }
465
466 // Here we need to check if $fkColumns are the same as $primaryKeys
467 if (! array_diff($fkColumns, $primaryKeys)) {
468 $metadata->mapOneToOne($associationMapping);
469 } else {
470 $metadata->mapManyToOne($associationMapping);
471 }
472 }
473 }
474
475 /**
476 * Retrieve schema table definition primary keys.
477 *
478 * @return string[]
479 */
480 private function getTablePrimaryKeys(Table $table): array
481 {
482 try {
483 return $table->getPrimaryKey()->getColumns();
484 } catch (SchemaException) {
485 // Do nothing
486 }
487
488 return [];
489 }
490
491 /**
492 * Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
493 *
494 * @psalm-return class-string
495 */
496 private function getClassNameForTable(string $tableName): string
497 {
498 if (isset($this->classNamesForTables[$tableName])) {
499 return $this->namespace . $this->classNamesForTables[$tableName];
500 }
501
502 return $this->namespace . $this->inflector->classify(strtolower($tableName));
503 }
504
505 /**
506 * Return the mapped field name for a column, if it exists. Otherwise return camelized version.
507 *
508 * @param bool $fk Whether the column is a foreignkey or not.
509 */
510 private function getFieldNameForColumn(
511 string $tableName,
512 string $columnName,
513 bool $fk = false,
514 ): string {
515 if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) {
516 return $this->fieldNamesForColumns[$tableName][$columnName];
517 }
518
519 $columnName = strtolower($columnName);
520
521 // Replace _id if it is a foreignkey column
522 if ($fk) {
523 $columnName = preg_replace('/_id$/', '', $columnName);
524 }
525
526 return $this->inflector->camelize($columnName);
527 }
528}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php
new file mode 100644
index 0000000..7d85471
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\ORM\Mapping\ClassMetadata;
8use ReflectionProperty;
9
10/** @internal */
11trait ReflectionBasedDriver
12{
13 /**
14 * Helps to deal with the case that reflection may report properties inherited from parent classes.
15 * When we know about the fields already (inheritance has been anticipated in ClassMetadataFactory),
16 * the driver must skip them.
17 *
18 * The declaring classes may mismatch when there are private properties: The same property name may be
19 * reported multiple times, but since it is private, it is in fact multiple (different) properties in
20 * different classes. In that case, report the property as an individual field. (ClassMetadataFactory will
21 * probably fail in that case, though.)
22 */
23 private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
24 {
25 $declaringClass = $property->class;
26
27 if (
28 isset($metadata->fieldMappings[$property->name]->declared)
29 && $metadata->fieldMappings[$property->name]->declared === $declaringClass
30 ) {
31 return true;
32 }
33
34 if (
35 isset($metadata->associationMappings[$property->name]->declared)
36 && $metadata->associationMappings[$property->name]->declared === $declaringClass
37 ) {
38 return true;
39 }
40
41 return isset($metadata->embeddedClasses[$property->name]->declared)
42 && $metadata->embeddedClasses[$property->name]->declared === $declaringClass;
43 }
44}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php b/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php
new file mode 100644
index 0000000..2f6ae93
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use ArrayObject;
8use Doctrine\ORM\Mapping\MappingAttribute;
9
10/**
11 * @template-extends ArrayObject<int, T>
12 * @template T of MappingAttribute
13 */
14final class RepeatableAttributeCollection extends ArrayObject
15{
16}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php
new file mode 100644
index 0000000..486185f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator;
8
9/**
10 * XmlDriver that additionally looks for mapping information in a global file.
11 */
12class SimplifiedXmlDriver extends XmlDriver
13{
14 public const DEFAULT_FILE_EXTENSION = '.orm.xml';
15
16 /**
17 * {@inheritDoc}
18 */
19 public function __construct($prefixes, $fileExtension = self::DEFAULT_FILE_EXTENSION, bool $isXsdValidationEnabled = true)
20 {
21 $locator = new SymfonyFileLocator((array) $prefixes, $fileExtension);
22
23 parent::__construct($locator, $fileExtension, $isXsdValidationEnabled);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php
new file mode 100644
index 0000000..ff473ce
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php
@@ -0,0 +1,940 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Driver;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\Common\Collections\Order;
9use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Mapping\MappingException;
12use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
13use Doctrine\Persistence\Mapping\Driver\FileDriver;
14use Doctrine\Persistence\Mapping\Driver\FileLocator;
15use DOMDocument;
16use InvalidArgumentException;
17use LogicException;
18use SimpleXMLElement;
19
20use function assert;
21use function constant;
22use function count;
23use function defined;
24use function enum_exists;
25use function explode;
26use function extension_loaded;
27use function file_get_contents;
28use function in_array;
29use function libxml_clear_errors;
30use function libxml_get_errors;
31use function libxml_use_internal_errors;
32use function simplexml_load_string;
33use function sprintf;
34use function str_replace;
35use function strtoupper;
36
37/**
38 * XmlDriver is a metadata driver that enables mapping through XML files.
39 *
40 * @link www.doctrine-project.org
41 *
42 * @template-extends FileDriver<SimpleXMLElement>
43 */
44class XmlDriver extends FileDriver
45{
46 public const DEFAULT_FILE_EXTENSION = '.dcm.xml';
47
48 /**
49 * {@inheritDoc}
50 */
51 public function __construct(
52 string|array|FileLocator $locator,
53 string $fileExtension = self::DEFAULT_FILE_EXTENSION,
54 private readonly bool $isXsdValidationEnabled = true,
55 ) {
56 if (! extension_loaded('simplexml')) {
57 throw new LogicException(
58 'The XML metadata driver cannot be enabled because the SimpleXML PHP extension is missing.'
59 . ' Please configure PHP with SimpleXML or choose a different metadata driver.',
60 );
61 }
62
63 if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
64 throw new LogicException(
65 'XSD validation cannot be enabled because the DOM extension is missing.',
66 );
67 }
68
69 parent::__construct($locator, $fileExtension);
70 }
71
72 /**
73 * {@inheritDoc}
74 *
75 * @psalm-param class-string<T> $className
76 * @psalm-param ClassMetadata<T> $metadata
77 *
78 * @template T of object
79 */
80 public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void
81 {
82 $xmlRoot = $this->getElement($className);
83
84 if ($xmlRoot->getName() === 'entity') {
85 if (isset($xmlRoot['repository-class'])) {
86 $metadata->setCustomRepositoryClass((string) $xmlRoot['repository-class']);
87 }
88
89 if (isset($xmlRoot['read-only']) && $this->evaluateBoolean($xmlRoot['read-only'])) {
90 $metadata->markReadOnly();
91 }
92 } elseif ($xmlRoot->getName() === 'mapped-superclass') {
93 $metadata->setCustomRepositoryClass(
94 isset($xmlRoot['repository-class']) ? (string) $xmlRoot['repository-class'] : null,
95 );
96 $metadata->isMappedSuperclass = true;
97 } elseif ($xmlRoot->getName() === 'embeddable') {
98 $metadata->isEmbeddedClass = true;
99 } else {
100 throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
101 }
102
103 // Evaluate <entity...> attributes
104 $primaryTable = [];
105
106 if (isset($xmlRoot['table'])) {
107 $primaryTable['name'] = (string) $xmlRoot['table'];
108 }
109
110 if (isset($xmlRoot['schema'])) {
111 $primaryTable['schema'] = (string) $xmlRoot['schema'];
112 }
113
114 $metadata->setPrimaryTable($primaryTable);
115
116 // Evaluate second level cache
117 if (isset($xmlRoot->cache)) {
118 $metadata->enableCache($this->cacheToArray($xmlRoot->cache));
119 }
120
121 if (isset($xmlRoot['inheritance-type'])) {
122 $inheritanceType = (string) $xmlRoot['inheritance-type'];
123 $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType));
124
125 if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
126 // Evaluate <discriminator-column...>
127 if (isset($xmlRoot->{'discriminator-column'})) {
128 $discrColumn = $xmlRoot->{'discriminator-column'};
129 $columnDef = [
130 'name' => isset($discrColumn['name']) ? (string) $discrColumn['name'] : null,
131 'type' => isset($discrColumn['type']) ? (string) $discrColumn['type'] : 'string',
132 'length' => isset($discrColumn['length']) ? (int) $discrColumn['length'] : 255,
133 'columnDefinition' => isset($discrColumn['column-definition']) ? (string) $discrColumn['column-definition'] : null,
134 'enumType' => isset($discrColumn['enum-type']) ? (string) $discrColumn['enum-type'] : null,
135 ];
136
137 if (isset($discrColumn['options'])) {
138 assert($discrColumn['options'] instanceof SimpleXMLElement);
139 $columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
140 }
141
142 $metadata->setDiscriminatorColumn($columnDef);
143 } else {
144 $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]);
145 }
146
147 // Evaluate <discriminator-map...>
148 if (isset($xmlRoot->{'discriminator-map'})) {
149 $map = [];
150 assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
151 foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
152 $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
153 }
154
155 $metadata->setDiscriminatorMap($map);
156 }
157 }
158 }
159
160 // Evaluate <change-tracking-policy...>
161 if (isset($xmlRoot['change-tracking-policy'])) {
162 $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_'
163 . strtoupper((string) $xmlRoot['change-tracking-policy'])));
164 }
165
166 // Evaluate <indexes...>
167 if (isset($xmlRoot->indexes)) {
168 $metadata->table['indexes'] = [];
169 foreach ($xmlRoot->indexes->index ?? [] as $indexXml) {
170 $index = [];
171
172 if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) {
173 $index['columns'] = explode(',', (string) $indexXml['columns']);
174 }
175
176 if (isset($indexXml['fields'])) {
177 $index['fields'] = explode(',', (string) $indexXml['fields']);
178 }
179
180 if (
181 isset($index['columns'], $index['fields'])
182 || (
183 ! isset($index['columns'])
184 && ! isset($index['fields'])
185 )
186 ) {
187 throw MappingException::invalidIndexConfiguration(
188 $className,
189 (string) ($indexXml['name'] ?? count($metadata->table['indexes'])),
190 );
191 }
192
193 if (isset($indexXml['flags'])) {
194 $index['flags'] = explode(',', (string) $indexXml['flags']);
195 }
196
197 if (isset($indexXml->options)) {
198 $index['options'] = $this->parseOptions($indexXml->options->children());
199 }
200
201 if (isset($indexXml['name'])) {
202 $metadata->table['indexes'][(string) $indexXml['name']] = $index;
203 } else {
204 $metadata->table['indexes'][] = $index;
205 }
206 }
207 }
208
209 // Evaluate <unique-constraints..>
210 if (isset($xmlRoot->{'unique-constraints'})) {
211 $metadata->table['uniqueConstraints'] = [];
212 foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) {
213 $unique = [];
214
215 if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) {
216 $unique['columns'] = explode(',', (string) $uniqueXml['columns']);
217 }
218
219 if (isset($uniqueXml['fields'])) {
220 $unique['fields'] = explode(',', (string) $uniqueXml['fields']);
221 }
222
223 if (
224 isset($unique['columns'], $unique['fields'])
225 || (
226 ! isset($unique['columns'])
227 && ! isset($unique['fields'])
228 )
229 ) {
230 throw MappingException::invalidUniqueConstraintConfiguration(
231 $className,
232 (string) ($uniqueXml['name'] ?? count($metadata->table['uniqueConstraints'])),
233 );
234 }
235
236 if (isset($uniqueXml->options)) {
237 $unique['options'] = $this->parseOptions($uniqueXml->options->children());
238 }
239
240 if (isset($uniqueXml['name'])) {
241 $metadata->table['uniqueConstraints'][(string) $uniqueXml['name']] = $unique;
242 } else {
243 $metadata->table['uniqueConstraints'][] = $unique;
244 }
245 }
246 }
247
248 if (isset($xmlRoot->options)) {
249 $metadata->table['options'] = $this->parseOptions($xmlRoot->options->children());
250 }
251
252 // The mapping assignment is done in 2 times as a bug might occurs on some php/xml lib versions
253 // The internal SimpleXmlIterator get resetted, to this generate a duplicate field exception
254 // Evaluate <field ...> mappings
255 if (isset($xmlRoot->field)) {
256 foreach ($xmlRoot->field as $fieldMapping) {
257 $mapping = $this->columnToArray($fieldMapping);
258
259 if (isset($mapping['version'])) {
260 $metadata->setVersionMapping($mapping);
261 unset($mapping['version']);
262 }
263
264 $metadata->mapField($mapping);
265 }
266 }
267
268 if (isset($xmlRoot->embedded)) {
269 foreach ($xmlRoot->embedded as $embeddedMapping) {
270 $columnPrefix = isset($embeddedMapping['column-prefix'])
271 ? (string) $embeddedMapping['column-prefix']
272 : null;
273
274 $useColumnPrefix = isset($embeddedMapping['use-column-prefix'])
275 ? $this->evaluateBoolean($embeddedMapping['use-column-prefix'])
276 : true;
277
278 $mapping = [
279 'fieldName' => (string) $embeddedMapping['name'],
280 'class' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null,
281 'columnPrefix' => $useColumnPrefix ? $columnPrefix : false,
282 ];
283
284 $metadata->mapEmbedded($mapping);
285 }
286 }
287
288 // Evaluate <id ...> mappings
289 $associationIds = [];
290 foreach ($xmlRoot->id ?? [] as $idElement) {
291 if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
292 $associationIds[(string) $idElement['name']] = true;
293 continue;
294 }
295
296 $mapping = $this->columnToArray($idElement);
297 $mapping['id'] = true;
298
299 $metadata->mapField($mapping);
300
301 if (isset($idElement->generator)) {
302 $strategy = isset($idElement->generator['strategy']) ?
303 (string) $idElement->generator['strategy'] : 'AUTO';
304 $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_'
305 . $strategy));
306 }
307
308 // Check for SequenceGenerator/TableGenerator definition
309 if (isset($idElement->{'sequence-generator'})) {
310 $seqGenerator = $idElement->{'sequence-generator'};
311 $metadata->setSequenceGeneratorDefinition(
312 [
313 'sequenceName' => (string) $seqGenerator['sequence-name'],
314 'allocationSize' => (string) $seqGenerator['allocation-size'],
315 'initialValue' => (string) $seqGenerator['initial-value'],
316 ],
317 );
318 } elseif (isset($idElement->{'custom-id-generator'})) {
319 $customGenerator = $idElement->{'custom-id-generator'};
320 $metadata->setCustomGeneratorDefinition(
321 [
322 'class' => (string) $customGenerator['class'],
323 ],
324 );
325 }
326 }
327
328 // Evaluate <one-to-one ...> mappings
329 if (isset($xmlRoot->{'one-to-one'})) {
330 foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) {
331 $mapping = [
332 'fieldName' => (string) $oneToOneElement['field'],
333 ];
334
335 if (isset($oneToOneElement['target-entity'])) {
336 $mapping['targetEntity'] = (string) $oneToOneElement['target-entity'];
337 }
338
339 if (isset($associationIds[$mapping['fieldName']])) {
340 $mapping['id'] = true;
341 }
342
343 if (isset($oneToOneElement['fetch'])) {
344 $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToOneElement['fetch']);
345 }
346
347 if (isset($oneToOneElement['mapped-by'])) {
348 $mapping['mappedBy'] = (string) $oneToOneElement['mapped-by'];
349 } else {
350 if (isset($oneToOneElement['inversed-by'])) {
351 $mapping['inversedBy'] = (string) $oneToOneElement['inversed-by'];
352 }
353
354 $joinColumns = [];
355
356 if (isset($oneToOneElement->{'join-column'})) {
357 $joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'});
358 } elseif (isset($oneToOneElement->{'join-columns'})) {
359 foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
360 $joinColumns[] = $this->joinColumnToArray($joinColumnElement);
361 }
362 }
363
364 $mapping['joinColumns'] = $joinColumns;
365 }
366
367 if (isset($oneToOneElement->cascade)) {
368 $mapping['cascade'] = $this->getCascadeMappings($oneToOneElement->cascade);
369 }
370
371 if (isset($oneToOneElement['orphan-removal'])) {
372 $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToOneElement['orphan-removal']);
373 }
374
375 // Evaluate second level cache
376 if (isset($oneToOneElement->cache)) {
377 $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache));
378 }
379
380 $metadata->mapOneToOne($mapping);
381 }
382 }
383
384 // Evaluate <one-to-many ...> mappings
385 if (isset($xmlRoot->{'one-to-many'})) {
386 foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) {
387 $mapping = [
388 'fieldName' => (string) $oneToManyElement['field'],
389 'mappedBy' => (string) $oneToManyElement['mapped-by'],
390 ];
391
392 if (isset($oneToManyElement['target-entity'])) {
393 $mapping['targetEntity'] = (string) $oneToManyElement['target-entity'];
394 }
395
396 if (isset($oneToManyElement['fetch'])) {
397 $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToManyElement['fetch']);
398 }
399
400 if (isset($oneToManyElement->cascade)) {
401 $mapping['cascade'] = $this->getCascadeMappings($oneToManyElement->cascade);
402 }
403
404 if (isset($oneToManyElement['orphan-removal'])) {
405 $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToManyElement['orphan-removal']);
406 }
407
408 if (isset($oneToManyElement->{'order-by'})) {
409 $orderBy = [];
410 foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
411 /** @psalm-suppress DeprecatedConstant */
412 $orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
413 ? (string) $orderByField['direction']
414 : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
415 }
416
417 $mapping['orderBy'] = $orderBy;
418 }
419
420 if (isset($oneToManyElement['index-by'])) {
421 $mapping['indexBy'] = (string) $oneToManyElement['index-by'];
422 } elseif (isset($oneToManyElement->{'index-by'})) {
423 throw new InvalidArgumentException('<index-by /> is not a valid tag');
424 }
425
426 // Evaluate second level cache
427 if (isset($oneToManyElement->cache)) {
428 $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache));
429 }
430
431 $metadata->mapOneToMany($mapping);
432 }
433 }
434
435 // Evaluate <many-to-one ...> mappings
436 if (isset($xmlRoot->{'many-to-one'})) {
437 foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) {
438 $mapping = [
439 'fieldName' => (string) $manyToOneElement['field'],
440 ];
441
442 if (isset($manyToOneElement['target-entity'])) {
443 $mapping['targetEntity'] = (string) $manyToOneElement['target-entity'];
444 }
445
446 if (isset($associationIds[$mapping['fieldName']])) {
447 $mapping['id'] = true;
448 }
449
450 if (isset($manyToOneElement['fetch'])) {
451 $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToOneElement['fetch']);
452 }
453
454 if (isset($manyToOneElement['inversed-by'])) {
455 $mapping['inversedBy'] = (string) $manyToOneElement['inversed-by'];
456 }
457
458 $joinColumns = [];
459
460 if (isset($manyToOneElement->{'join-column'})) {
461 $joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'});
462 } elseif (isset($manyToOneElement->{'join-columns'})) {
463 foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
464 $joinColumns[] = $this->joinColumnToArray($joinColumnElement);
465 }
466 }
467
468 $mapping['joinColumns'] = $joinColumns;
469
470 if (isset($manyToOneElement->cascade)) {
471 $mapping['cascade'] = $this->getCascadeMappings($manyToOneElement->cascade);
472 }
473
474 // Evaluate second level cache
475 if (isset($manyToOneElement->cache)) {
476 $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache));
477 }
478
479 $metadata->mapManyToOne($mapping);
480 }
481 }
482
483 // Evaluate <many-to-many ...> mappings
484 if (isset($xmlRoot->{'many-to-many'})) {
485 foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) {
486 $mapping = [
487 'fieldName' => (string) $manyToManyElement['field'],
488 ];
489
490 if (isset($manyToManyElement['target-entity'])) {
491 $mapping['targetEntity'] = (string) $manyToManyElement['target-entity'];
492 }
493
494 if (isset($manyToManyElement['fetch'])) {
495 $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToManyElement['fetch']);
496 }
497
498 if (isset($manyToManyElement['orphan-removal'])) {
499 $mapping['orphanRemoval'] = $this->evaluateBoolean($manyToManyElement['orphan-removal']);
500 }
501
502 if (isset($manyToManyElement['mapped-by'])) {
503 $mapping['mappedBy'] = (string) $manyToManyElement['mapped-by'];
504 } elseif (isset($manyToManyElement->{'join-table'})) {
505 if (isset($manyToManyElement['inversed-by'])) {
506 $mapping['inversedBy'] = (string) $manyToManyElement['inversed-by'];
507 }
508
509 $joinTableElement = $manyToManyElement->{'join-table'};
510 $joinTable = [
511 'name' => (string) $joinTableElement['name'],
512 ];
513
514 if (isset($joinTableElement['schema'])) {
515 $joinTable['schema'] = (string) $joinTableElement['schema'];
516 }
517
518 if (isset($joinTableElement->options)) {
519 $joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
520 }
521
522 foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
523 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
524 }
525
526 foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
527 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
528 }
529
530 $mapping['joinTable'] = $joinTable;
531 }
532
533 if (isset($manyToManyElement->cascade)) {
534 $mapping['cascade'] = $this->getCascadeMappings($manyToManyElement->cascade);
535 }
536
537 if (isset($manyToManyElement->{'order-by'})) {
538 $orderBy = [];
539 foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
540 /** @psalm-suppress DeprecatedConstant */
541 $orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
542 ? (string) $orderByField['direction']
543 : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
544 }
545
546 $mapping['orderBy'] = $orderBy;
547 }
548
549 if (isset($manyToManyElement['index-by'])) {
550 $mapping['indexBy'] = (string) $manyToManyElement['index-by'];
551 } elseif (isset($manyToManyElement->{'index-by'})) {
552 throw new InvalidArgumentException('<index-by /> is not a valid tag');
553 }
554
555 // Evaluate second level cache
556 if (isset($manyToManyElement->cache)) {
557 $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache));
558 }
559
560 $metadata->mapManyToMany($mapping);
561 }
562 }
563
564 // Evaluate association-overrides
565 if (isset($xmlRoot->{'attribute-overrides'})) {
566 foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) {
567 $fieldName = (string) $overrideElement['name'];
568 foreach ($overrideElement->field ?? [] as $field) {
569 $mapping = $this->columnToArray($field);
570 $mapping['fieldName'] = $fieldName;
571 $metadata->setAttributeOverride($fieldName, $mapping);
572 }
573 }
574 }
575
576 // Evaluate association-overrides
577 if (isset($xmlRoot->{'association-overrides'})) {
578 foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) {
579 $fieldName = (string) $overrideElement['name'];
580 $override = [];
581
582 // Check for join-columns
583 if (isset($overrideElement->{'join-columns'})) {
584 $joinColumns = [];
585 foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
586 $joinColumns[] = $this->joinColumnToArray($joinColumnElement);
587 }
588
589 $override['joinColumns'] = $joinColumns;
590 }
591
592 // Check for join-table
593 if ($overrideElement->{'join-table'}) {
594 $joinTable = null;
595 $joinTableElement = $overrideElement->{'join-table'};
596
597 $joinTable = [
598 'name' => (string) $joinTableElement['name'],
599 'schema' => (string) $joinTableElement['schema'],
600 ];
601
602 if (isset($joinTableElement->options)) {
603 $joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
604 }
605
606 if (isset($joinTableElement->{'join-columns'})) {
607 foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
608 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
609 }
610 }
611
612 if (isset($joinTableElement->{'inverse-join-columns'})) {
613 foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
614 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
615 }
616 }
617
618 $override['joinTable'] = $joinTable;
619 }
620
621 // Check for inversed-by
622 if (isset($overrideElement->{'inversed-by'})) {
623 $override['inversedBy'] = (string) $overrideElement->{'inversed-by'}['name'];
624 }
625
626 // Check for `fetch`
627 if (isset($overrideElement['fetch'])) {
628 $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . (string) $overrideElement['fetch']);
629 }
630
631 $metadata->setAssociationOverride($fieldName, $override);
632 }
633 }
634
635 // Evaluate <lifecycle-callbacks...>
636 if (isset($xmlRoot->{'lifecycle-callbacks'})) {
637 foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) {
638 $metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type']));
639 }
640 }
641
642 // Evaluate entity listener
643 if (isset($xmlRoot->{'entity-listeners'})) {
644 foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) {
645 $className = (string) $listenerElement['class'];
646 // Evaluate the listener using naming convention.
647 if ($listenerElement->count() === 0) {
648 EntityListenerBuilder::bindEntityListener($metadata, $className);
649
650 continue;
651 }
652
653 foreach ($listenerElement as $callbackElement) {
654 $eventName = (string) $callbackElement['type'];
655 $methodName = (string) $callbackElement['method'];
656
657 $metadata->addEntityListener($eventName, $className, $methodName);
658 }
659 }
660 }
661 }
662
663 /**
664 * Parses (nested) option elements.
665 *
666 * @return mixed[] The options array.
667 * @psalm-return array<int|string, array<int|string, mixed|string>|bool|string>
668 */
669 private function parseOptions(SimpleXMLElement|null $options): array
670 {
671 $array = [];
672
673 foreach ($options ?? [] as $option) {
674 if ($option->count()) {
675 $value = $this->parseOptions($option->children());
676 } else {
677 $value = (string) $option;
678 }
679
680 $attributes = $option->attributes();
681
682 if (isset($attributes->name)) {
683 $nameAttribute = (string) $attributes->name;
684 $array[$nameAttribute] = in_array($nameAttribute, ['unsigned', 'fixed'], true)
685 ? $this->evaluateBoolean($value)
686 : $value;
687 } else {
688 $array[] = $value;
689 }
690 }
691
692 return $array;
693 }
694
695 /**
696 * Constructs a joinColumn mapping array based on the information
697 * found in the given SimpleXMLElement.
698 *
699 * @param SimpleXMLElement $joinColumnElement The XML element.
700 *
701 * @return mixed[] The mapping array.
702 * @psalm-return array{
703 * name: string,
704 * referencedColumnName: string,
705 * unique?: bool,
706 * nullable?: bool,
707 * onDelete?: string,
708 * columnDefinition?: string,
709 * options?: mixed[]
710 * }
711 */
712 private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array
713 {
714 $joinColumn = [
715 'name' => (string) $joinColumnElement['name'],
716 'referencedColumnName' => (string) $joinColumnElement['referenced-column-name'],
717 ];
718
719 if (isset($joinColumnElement['unique'])) {
720 $joinColumn['unique'] = $this->evaluateBoolean($joinColumnElement['unique']);
721 }
722
723 if (isset($joinColumnElement['nullable'])) {
724 $joinColumn['nullable'] = $this->evaluateBoolean($joinColumnElement['nullable']);
725 }
726
727 if (isset($joinColumnElement['on-delete'])) {
728 $joinColumn['onDelete'] = (string) $joinColumnElement['on-delete'];
729 }
730
731 if (isset($joinColumnElement['column-definition'])) {
732 $joinColumn['columnDefinition'] = (string) $joinColumnElement['column-definition'];
733 }
734
735 if (isset($joinColumnElement['options'])) {
736 $joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null);
737 }
738
739 return $joinColumn;
740 }
741
742 /**
743 * Parses the given field as array.
744 *
745 * @return mixed[]
746 * @psalm-return array{
747 * fieldName: string,
748 * type?: string,
749 * columnName?: string,
750 * length?: int,
751 * precision?: int,
752 * scale?: int,
753 * unique?: bool,
754 * nullable?: bool,
755 * notInsertable?: bool,
756 * notUpdatable?: bool,
757 * enumType?: string,
758 * version?: bool,
759 * columnDefinition?: string,
760 * options?: array
761 * }
762 */
763 private function columnToArray(SimpleXMLElement $fieldMapping): array
764 {
765 $mapping = [
766 'fieldName' => (string) $fieldMapping['name'],
767 ];
768
769 if (isset($fieldMapping['type'])) {
770 $mapping['type'] = (string) $fieldMapping['type'];
771 }
772
773 if (isset($fieldMapping['column'])) {
774 $mapping['columnName'] = (string) $fieldMapping['column'];
775 }
776
777 if (isset($fieldMapping['length'])) {
778 $mapping['length'] = (int) $fieldMapping['length'];
779 }
780
781 if (isset($fieldMapping['precision'])) {
782 $mapping['precision'] = (int) $fieldMapping['precision'];
783 }
784
785 if (isset($fieldMapping['scale'])) {
786 $mapping['scale'] = (int) $fieldMapping['scale'];
787 }
788
789 if (isset($fieldMapping['unique'])) {
790 $mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']);
791 }
792
793 if (isset($fieldMapping['nullable'])) {
794 $mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']);
795 }
796
797 if (isset($fieldMapping['insertable']) && ! $this->evaluateBoolean($fieldMapping['insertable'])) {
798 $mapping['notInsertable'] = true;
799 }
800
801 if (isset($fieldMapping['updatable']) && ! $this->evaluateBoolean($fieldMapping['updatable'])) {
802 $mapping['notUpdatable'] = true;
803 }
804
805 if (isset($fieldMapping['generated'])) {
806 $mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . (string) $fieldMapping['generated']);
807 }
808
809 if (isset($fieldMapping['version']) && $fieldMapping['version']) {
810 $mapping['version'] = $this->evaluateBoolean($fieldMapping['version']);
811 }
812
813 if (isset($fieldMapping['column-definition'])) {
814 $mapping['columnDefinition'] = (string) $fieldMapping['column-definition'];
815 }
816
817 if (isset($fieldMapping['enum-type'])) {
818 $mapping['enumType'] = (string) $fieldMapping['enum-type'];
819 }
820
821 if (isset($fieldMapping->options)) {
822 $mapping['options'] = $this->parseOptions($fieldMapping->options->children());
823 }
824
825 return $mapping;
826 }
827
828 /**
829 * Parse / Normalize the cache configuration
830 *
831 * @return mixed[]
832 * @psalm-return array{usage: int|null, region?: string}
833 */
834 private function cacheToArray(SimpleXMLElement $cacheMapping): array
835 {
836 $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null;
837 $usage = isset($cacheMapping['usage']) ? strtoupper((string) $cacheMapping['usage']) : null;
838
839 if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) {
840 throw new InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage));
841 }
842
843 if ($usage) {
844 $usage = (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage);
845 }
846
847 return [
848 'usage' => $usage,
849 'region' => $region,
850 ];
851 }
852
853 /**
854 * Gathers a list of cascade options found in the given cascade element.
855 *
856 * @param SimpleXMLElement $cascadeElement The cascade element.
857 *
858 * @return string[] The list of cascade options.
859 * @psalm-return list<string>
860 */
861 private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
862 {
863 $cascades = [];
864 $children = $cascadeElement->children();
865 assert($children !== null);
866
867 foreach ($children as $action) {
868 // According to the JPA specifications, XML uses "cascade-persist"
869 // instead of "persist". Here, both variations
870 // are supported because Attribute uses "persist"
871 // and we want to make sure that this driver doesn't need to know
872 // anything about the supported cascading actions
873 $cascades[] = str_replace('cascade-', '', $action->getName());
874 }
875
876 return $cascades;
877 }
878
879 /**
880 * {@inheritDoc}
881 */
882 protected function loadMappingFile($file)
883 {
884 $this->validateMapping($file);
885 $result = [];
886 // Note: we do not use `simplexml_load_file()` because of https://bugs.php.net/bug.php?id=62577
887 $xmlElement = simplexml_load_string(file_get_contents($file));
888 assert($xmlElement !== false);
889
890 if (isset($xmlElement->entity)) {
891 foreach ($xmlElement->entity as $entityElement) {
892 /** @psalm-var class-string $entityName */
893 $entityName = (string) $entityElement['name'];
894 $result[$entityName] = $entityElement;
895 }
896 } elseif (isset($xmlElement->{'mapped-superclass'})) {
897 foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
898 /** @psalm-var class-string $className */
899 $className = (string) $mappedSuperClass['name'];
900 $result[$className] = $mappedSuperClass;
901 }
902 } elseif (isset($xmlElement->embeddable)) {
903 foreach ($xmlElement->embeddable as $embeddableElement) {
904 /** @psalm-var class-string $embeddableName */
905 $embeddableName = (string) $embeddableElement['name'];
906 $result[$embeddableName] = $embeddableElement;
907 }
908 }
909
910 return $result;
911 }
912
913 private function validateMapping(string $file): void
914 {
915 if (! $this->isXsdValidationEnabled) {
916 return;
917 }
918
919 $backedUpErrorSetting = libxml_use_internal_errors(true);
920
921 try {
922 $document = new DOMDocument();
923 $document->load($file);
924
925 if (! $document->schemaValidate(__DIR__ . '/../../../doctrine-mapping.xsd')) {
926 throw MappingException::fromLibXmlErrors(libxml_get_errors());
927 }
928 } finally {
929 libxml_clear_errors();
930 libxml_use_internal_errors($backedUpErrorSetting);
931 }
932 }
933
934 protected function evaluateBoolean(mixed $element): bool
935 {
936 $flag = (string) $element;
937
938 return $flag === 'true' || $flag === '1';
939 }
940}
diff --git a/vendor/doctrine/orm/src/Mapping/Embeddable.php b/vendor/doctrine/orm/src/Mapping/Embeddable.php
new file mode 100644
index 0000000..b8dfea0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Embeddable.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS)]
10final class Embeddable implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/Embedded.php b/vendor/doctrine/orm/src/Mapping/Embedded.php
new file mode 100644
index 0000000..be69b4f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Embedded.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class Embedded implements MappingAttribute
11{
12 public function __construct(
13 public readonly string|null $class = null,
14 public readonly string|bool|null $columnPrefix = null,
15 ) {
16 }
17}
diff --git a/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php b/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php
new file mode 100644
index 0000000..8fd02c9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php
@@ -0,0 +1,93 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8
9use function property_exists;
10
11/** @template-implements ArrayAccess<string, mixed> */
12final class EmbeddedClassMapping implements ArrayAccess
13{
14 use ArrayAccessImplementation;
15
16 public string|false|null $columnPrefix = null;
17 public string|null $declaredField = null;
18 public string|null $originalField = null;
19
20 /**
21 * This is set when this embedded-class field is inherited by this class
22 * from another (inheritance) parent <em>entity</em> class. The value is
23 * the FQCN of the topmost entity class that contains mapping information
24 * for this field. (If there are transient classes in the class hierarchy,
25 * these are ignored, so the class property may in fact come from a class
26 * further up in the PHP class hierarchy.) Fields initially declared in
27 * mapped superclasses are <em>not</em> considered 'inherited' in the
28 * nearest entity subclasses.
29 *
30 * @var class-string|null
31 */
32 public string|null $inherited = null;
33
34 /**
35 * This is set when the embedded-class field does not appear for the first
36 * time in this class, but is originally declared in another parent
37 * <em>entity or mapped superclass</em>. The value is the FQCN of the
38 * topmost non-transient class that contains mapping information for this
39 * field.
40 *
41 * @var class-string|null
42 */
43 public string|null $declared = null;
44
45 /** @param class-string $class */
46 public function __construct(public string $class)
47 {
48 }
49
50 /**
51 * @psalm-param array{
52 * class: class-string,
53 * columnPrefix?: false|string|null,
54 * declaredField?: string|null,
55 * originalField?: string|null,
56 * inherited?: class-string|null,
57 * declared?: class-string|null,
58 * } $mappingArray
59 */
60 public static function fromMappingArray(array $mappingArray): self
61 {
62 $mapping = new self($mappingArray['class']);
63 foreach ($mappingArray as $key => $value) {
64 if ($key === 'class') {
65 continue;
66 }
67
68 if (property_exists($mapping, $key)) {
69 $mapping->$key = $value;
70 }
71 }
72
73 return $mapping;
74 }
75
76 /** @return list<string> */
77 public function __sleep(): array
78 {
79 $serialized = ['class'];
80
81 if ($this->columnPrefix) {
82 $serialized[] = 'columnPrefix';
83 }
84
85 foreach (['declaredField', 'originalField', 'inherited', 'declared'] as $property) {
86 if ($this->$property !== null) {
87 $serialized[] = $property;
88 }
89 }
90
91 return $serialized;
92 }
93}
diff --git a/vendor/doctrine/orm/src/Mapping/Entity.php b/vendor/doctrine/orm/src/Mapping/Entity.php
new file mode 100644
index 0000000..0e27913
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Entity.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8use Doctrine\ORM\EntityRepository;
9
10/** @template T of object */
11#[Attribute(Attribute::TARGET_CLASS)]
12final class Entity implements MappingAttribute
13{
14 /** @psalm-param class-string<EntityRepository<T>>|null $repositoryClass */
15 public function __construct(
16 public readonly string|null $repositoryClass = null,
17 public readonly bool $readOnly = false,
18 ) {
19 }
20}
diff --git a/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php b/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php
new file mode 100644
index 0000000..eabc217
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php
@@ -0,0 +1,30 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/**
8 * A resolver is used to instantiate an entity listener.
9 */
10interface EntityListenerResolver
11{
12 /**
13 * Clear all instances from the set, or a specific instance when given its identifier.
14 *
15 * @param string|null $className May be any arbitrary string. Name kept for BC only.
16 */
17 public function clear(string|null $className = null): void;
18
19 /**
20 * Returns a entity listener instance for the given identifier.
21 *
22 * @param string $className May be any arbitrary string. Name kept for BC only.
23 */
24 public function resolve(string $className): object;
25
26 /**
27 * Register a entity listener instance.
28 */
29 public function register(object $object): void;
30}
diff --git a/vendor/doctrine/orm/src/Mapping/EntityListeners.php b/vendor/doctrine/orm/src/Mapping/EntityListeners.php
new file mode 100644
index 0000000..8f822ea
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/EntityListeners.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9/**
10 * The EntityListeners attribute specifies the callback listener classes to be used for an entity or mapped superclass.
11 * The EntityListeners attribute may be applied to an entity class or mapped superclass.
12 */
13#[Attribute(Attribute::TARGET_CLASS)]
14final class EntityListeners implements MappingAttribute
15{
16 /** @param array<string> $value */
17 public function __construct(
18 public readonly array $value = [],
19 ) {
20 }
21}
diff --git a/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php b/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php
new file mode 100644
index 0000000..b9e10bf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10use function sprintf;
11use function var_export;
12
13final class InvalidCustomGenerator extends LogicException implements ORMException
14{
15 public static function onClassNotConfigured(): self
16 {
17 return new self('Cannot instantiate custom generator, no class has been defined');
18 }
19
20 /** @param mixed[] $definition */
21 public static function onMissingClass(array $definition): self
22 {
23 return new self(sprintf(
24 'Cannot instantiate custom generator : %s',
25 var_export($definition, true),
26 ));
27 }
28}
diff --git a/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php b/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php
new file mode 100644
index 0000000..c8970bf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10final class UnknownGeneratorType extends LogicException implements ORMException
11{
12 public static function create(int $generatorType): self
13 {
14 return new self('Unknown generator type: ' . $generatorType);
15 }
16}
diff --git a/vendor/doctrine/orm/src/Mapping/FieldMapping.php b/vendor/doctrine/orm/src/Mapping/FieldMapping.php
new file mode 100644
index 0000000..4c09196
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/FieldMapping.php
@@ -0,0 +1,169 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8use BackedEnum;
9
10use function in_array;
11use function property_exists;
12
13/** @template-implements ArrayAccess<string, mixed> */
14final class FieldMapping implements ArrayAccess
15{
16 use ArrayAccessImplementation;
17
18 /** The database length of the column. Optional. Default value taken from the type. */
19 public int|null $length = null;
20 /**
21 * Marks the field as the primary key of the entity. Multiple
22 * fields of an entity can have the id attribute, forming a composite key.
23 */
24 public bool|null $id = null;
25 public bool|null $nullable = null;
26 public bool|null $notInsertable = null;
27 public bool|null $notUpdatable = null;
28 public string|null $columnDefinition = null;
29 /** @psalm-var ClassMetadata::GENERATED_*|null */
30 public int|null $generated = null;
31 /** @var class-string<BackedEnum>|null */
32 public string|null $enumType = null;
33 /**
34 * The precision of a decimal column.
35 * Only valid if the column type is decimal
36 */
37 public int|null $precision = null;
38 /**
39 * The scale of a decimal column.
40 * Only valid if the column type is decimal
41 */
42 public int|null $scale = null;
43 /** Whether a unique constraint should be generated for the column. */
44 public bool|null $unique = null;
45 /**
46 * @var class-string|null This is set when the field is inherited by this
47 * class from another (inheritance) parent <em>entity</em> class. The value
48 * is the FQCN of the topmost entity class that contains mapping information
49 * for this field. (If there are transient classes in the class hierarchy,
50 * these are ignored, so the class property may in fact come from a class
51 * further up in the PHP class hierarchy.)
52 * Fields initially declared in mapped superclasses are
53 * <em>not</em> considered 'inherited' in the nearest entity subclasses.
54 */
55 public string|null $inherited = null;
56
57 public string|null $originalClass = null;
58 public string|null $originalField = null;
59 public bool|null $quoted = null;
60 /**
61 * @var class-string|null This is set when the field does not appear for
62 * the first time in this class, but is originally declared in another
63 * parent <em>entity or mapped superclass</em>. The value is the FQCN of
64 * the topmost non-transient class that contains mapping information for
65 * this field.
66 */
67 public string|null $declared = null;
68 public string|null $declaredField = null;
69 public array|null $options = null;
70 public bool|null $version = null;
71 public string|int|null $default = null;
72
73 /**
74 * @param string $type The type name of the mapped field. Can be one of
75 * Doctrine's mapping types or a custom mapping type.
76 * @param string $fieldName The name of the field in the Entity.
77 * @param string $columnName The column name. Optional. Defaults to the field name.
78 */
79 public function __construct(
80 public string $type,
81 public string $fieldName,
82 public string $columnName,
83 ) {
84 }
85
86 /**
87 * @param array<string, mixed> $mappingArray
88 * @psalm-param array{
89 * type: string,
90 * fieldName: string,
91 * columnName: string,
92 * length?: int|null,
93 * id?: bool|null,
94 * nullable?: bool|null,
95 * notInsertable?: bool|null,
96 * notUpdatable?: bool|null,
97 * columnDefinition?: string|null,
98 * generated?: ClassMetadata::GENERATED_*|null,
99 * enumType?: string|null,
100 * precision?: int|null,
101 * scale?: int|null,
102 * unique?: bool|null,
103 * inherited?: string|null,
104 * originalClass?: string|null,
105 * originalField?: string|null,
106 * quoted?: bool|null,
107 * declared?: string|null,
108 * declaredField?: string|null,
109 * options?: array<string, mixed>|null,
110 * version?: bool|null,
111 * default?: string|int|null,
112 * } $mappingArray
113 */
114 public static function fromMappingArray(array $mappingArray): self
115 {
116 $mapping = new self(
117 $mappingArray['type'],
118 $mappingArray['fieldName'],
119 $mappingArray['columnName'],
120 );
121 foreach ($mappingArray as $key => $value) {
122 if (in_array($key, ['type', 'fieldName', 'columnName'])) {
123 continue;
124 }
125
126 if (property_exists($mapping, $key)) {
127 $mapping->$key = $value;
128 }
129 }
130
131 return $mapping;
132 }
133
134 /** @return list<string> */
135 public function __sleep(): array
136 {
137 $serialized = ['type', 'fieldName', 'columnName'];
138
139 foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
140 if ($this->$boolKey) {
141 $serialized[] = $boolKey;
142 }
143 }
144
145 foreach (
146 [
147 'length',
148 'columnDefinition',
149 'generated',
150 'enumType',
151 'precision',
152 'scale',
153 'inherited',
154 'originalClass',
155 'originalField',
156 'declared',
157 'declaredField',
158 'options',
159 'default',
160 ] as $key
161 ) {
162 if ($this->$key !== null) {
163 $serialized[] = $key;
164 }
165 }
166
167 return $serialized;
168 }
169}
diff --git a/vendor/doctrine/orm/src/Mapping/GeneratedValue.php b/vendor/doctrine/orm/src/Mapping/GeneratedValue.php
new file mode 100644
index 0000000..aca5f4b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/GeneratedValue.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class GeneratedValue implements MappingAttribute
11{
12 /** @psalm-param 'AUTO'|'SEQUENCE'|'IDENTITY'|'NONE'|'CUSTOM' $strategy */
13 public function __construct(
14 public readonly string $strategy = 'AUTO',
15 ) {
16 }
17}
diff --git a/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php b/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php
new file mode 100644
index 0000000..d41a696
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS)]
10final class HasLifecycleCallbacks implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/Id.php b/vendor/doctrine/orm/src/Mapping/Id.php
new file mode 100644
index 0000000..e85dd2f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Id.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class Id implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/Index.php b/vendor/doctrine/orm/src/Mapping/Index.php
new file mode 100644
index 0000000..1de939e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Index.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
10final class Index implements MappingAttribute
11{
12 /**
13 * @param array<string>|null $columns
14 * @param array<string>|null $fields
15 * @param array<string>|null $flags
16 * @param array<string,mixed>|null $options
17 */
18 public function __construct(
19 public readonly string|null $name = null,
20 public readonly array|null $columns = null,
21 public readonly array|null $fields = null,
22 public readonly array|null $flags = null,
23 public readonly array|null $options = null,
24 ) {
25 }
26}
diff --git a/vendor/doctrine/orm/src/Mapping/InheritanceType.php b/vendor/doctrine/orm/src/Mapping/InheritanceType.php
new file mode 100644
index 0000000..c042ee7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/InheritanceType.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS)]
10final class InheritanceType implements MappingAttribute
11{
12 /** @psalm-param 'NONE'|'JOINED'|'SINGLE_TABLE' $value */
13 public function __construct(
14 public readonly string $value,
15 ) {
16 }
17}
diff --git a/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php b/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php
new file mode 100644
index 0000000..2a77f3f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
10final class InverseJoinColumn implements MappingAttribute
11{
12 use JoinColumnProperties;
13}
diff --git a/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php
new file mode 100644
index 0000000..56dce9f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php
@@ -0,0 +1,30 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7abstract class InverseSideMapping extends AssociationMapping
8{
9 /**
10 * required for bidirectional associations
11 * The name of the field that completes the bidirectional association on
12 * the owning side. This key must be specified on the inverse side of a
13 * bidirectional association.
14 */
15 public string $mappedBy;
16
17 final public function backRefFieldName(): string
18 {
19 return $this->mappedBy;
20 }
21
22 /** @return list<string> */
23 public function __sleep(): array
24 {
25 return [
26 ...parent::__sleep(),
27 'mappedBy',
28 ];
29 }
30}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumn.php b/vendor/doctrine/orm/src/Mapping/JoinColumn.php
new file mode 100644
index 0000000..9a049fb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinColumn.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
10final class JoinColumn implements MappingAttribute
11{
12 use JoinColumnProperties;
13}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php b/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php
new file mode 100644
index 0000000..172c256
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php
@@ -0,0 +1,77 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8
9use function property_exists;
10
11/** @template-implements ArrayAccess<string, mixed> */
12final class JoinColumnMapping implements ArrayAccess
13{
14 use ArrayAccessImplementation;
15
16 public bool|null $unique = null;
17 public bool|null $quoted = null;
18 public string|null $fieldName = null;
19 public string|null $onDelete = null;
20 public string|null $columnDefinition = null;
21 public bool|null $nullable = null;
22
23 /** @var array<string, mixed>|null */
24 public array|null $options = null;
25
26 public function __construct(
27 public string $name,
28 public string $referencedColumnName,
29 ) {
30 }
31
32 /**
33 * @param array<string, mixed> $mappingArray
34 * @psalm-param array{
35 * name: string,
36 * referencedColumnName: string,
37 * unique?: bool|null,
38 * quoted?: bool|null,
39 * fieldName?: string|null,
40 * onDelete?: string|null,
41 * columnDefinition?: string|null,
42 * nullable?: bool|null,
43 * options?: array<string, mixed>|null,
44 * } $mappingArray
45 */
46 public static function fromMappingArray(array $mappingArray): self
47 {
48 $mapping = new self($mappingArray['name'], $mappingArray['referencedColumnName']);
49 foreach ($mappingArray as $key => $value) {
50 if (property_exists($mapping, $key) && $value !== null) {
51 $mapping->$key = $value;
52 }
53 }
54
55 return $mapping;
56 }
57
58 /** @return list<string> */
59 public function __sleep(): array
60 {
61 $serialized = [];
62
63 foreach (['name', 'fieldName', 'onDelete', 'columnDefinition', 'referencedColumnName', 'options'] as $stringOrArrayKey) {
64 if ($this->$stringOrArrayKey !== null) {
65 $serialized[] = $stringOrArrayKey;
66 }
67 }
68
69 foreach (['unique', 'quoted', 'nullable'] as $boolKey) {
70 if ($this->$boolKey !== null) {
71 $serialized[] = $boolKey;
72 }
73 }
74
75 return $serialized;
76 }
77}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php b/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php
new file mode 100644
index 0000000..7d13295
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7trait JoinColumnProperties
8{
9 /** @param array<string, mixed> $options */
10 public function __construct(
11 public readonly string|null $name = null,
12 public readonly string $referencedColumnName = 'id',
13 public readonly bool $unique = false,
14 public readonly bool $nullable = true,
15 public readonly mixed $onDelete = null,
16 public readonly string|null $columnDefinition = null,
17 public readonly string|null $fieldName = null,
18 public readonly array $options = [],
19 ) {
20 }
21}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumns.php b/vendor/doctrine/orm/src/Mapping/JoinColumns.php
new file mode 100644
index 0000000..f166b76
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinColumns.php
@@ -0,0 +1,14 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7final class JoinColumns implements MappingAttribute
8{
9 /** @param array<JoinColumn> $value */
10 public function __construct(
11 public readonly array $value,
12 ) {
13 }
14}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinTable.php b/vendor/doctrine/orm/src/Mapping/JoinTable.php
new file mode 100644
index 0000000..0558761
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinTable.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class JoinTable implements MappingAttribute
11{
12 /** @var array<JoinColumn> */
13 public readonly array $joinColumns;
14
15 /** @var array<JoinColumn> */
16 public readonly array $inverseJoinColumns;
17
18 /**
19 * @param array<JoinColumn>|JoinColumn $joinColumns
20 * @param array<JoinColumn>|JoinColumn $inverseJoinColumns
21 * @param array<string, mixed> $options
22 */
23 public function __construct(
24 public readonly string|null $name = null,
25 public readonly string|null $schema = null,
26 array|JoinColumn $joinColumns = [],
27 array|JoinColumn $inverseJoinColumns = [],
28 public readonly array $options = [],
29 ) {
30 $this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns;
31 $this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn
32 ? [$inverseJoinColumns]
33 : $inverseJoinColumns;
34 }
35}
diff --git a/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php b/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php
new file mode 100644
index 0000000..c8b4968
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php
@@ -0,0 +1,115 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use ArrayAccess;
8
9use function array_map;
10use function in_array;
11
12/** @template-implements ArrayAccess<string, mixed> */
13final class JoinTableMapping implements ArrayAccess
14{
15 use ArrayAccessImplementation;
16
17 public bool|null $quoted = null;
18
19 /** @var list<JoinColumnMapping> */
20 public array $joinColumns = [];
21
22 /** @var list<JoinColumnMapping> */
23 public array $inverseJoinColumns = [];
24
25 /** @var array<string, mixed> */
26 public array $options = [];
27
28 public string|null $schema = null;
29
30 public function __construct(public string $name)
31 {
32 }
33
34 /**
35 * @param mixed[] $mappingArray
36 * @psalm-param array{
37 * name: string,
38 * quoted?: bool|null,
39 * joinColumns?: mixed[],
40 * inverseJoinColumns?: mixed[],
41 * schema?: string|null,
42 * options?: array<string, mixed>
43 * } $mappingArray
44 */
45 public static function fromMappingArray(array $mappingArray): self
46 {
47 $mapping = new self($mappingArray['name']);
48
49 foreach (['quoted', 'schema', 'options'] as $key) {
50 if (isset($mappingArray[$key])) {
51 $mapping->$key = $mappingArray[$key];
52 }
53 }
54
55 if (isset($mappingArray['joinColumns'])) {
56 foreach ($mappingArray['joinColumns'] as $column) {
57 $mapping->joinColumns[] = JoinColumnMapping::fromMappingArray($column);
58 }
59 }
60
61 if (isset($mappingArray['inverseJoinColumns'])) {
62 foreach ($mappingArray['inverseJoinColumns'] as $column) {
63 $mapping->inverseJoinColumns[] = JoinColumnMapping::fromMappingArray($column);
64 }
65 }
66
67 return $mapping;
68 }
69
70 public function offsetSet(mixed $offset, mixed $value): void
71 {
72 if (in_array($offset, ['joinColumns', 'inverseJoinColumns'], true)) {
73 $joinColumns = [];
74 foreach ($value as $column) {
75 $joinColumns[] = JoinColumnMapping::fromMappingArray($column);
76 }
77
78 $value = $joinColumns;
79 }
80
81 $this->$offset = $value;
82 }
83
84 /** @return mixed[] */
85 public function toArray(): array
86 {
87 $array = (array) $this;
88
89 $toArray = static fn (JoinColumnMapping $column): array => (array) $column;
90 $array['joinColumns'] = array_map($toArray, $array['joinColumns']);
91 $array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']);
92
93 return $array;
94 }
95
96 /** @return list<string> */
97 public function __sleep(): array
98 {
99 $serialized = [];
100
101 foreach (['joinColumns', 'inverseJoinColumns', 'name', 'schema', 'options'] as $stringOrArrayKey) {
102 if ($this->$stringOrArrayKey !== null) {
103 $serialized[] = $stringOrArrayKey;
104 }
105 }
106
107 foreach (['quoted'] as $boolKey) {
108 if ($this->$boolKey) {
109 $serialized[] = $boolKey;
110 }
111 }
112
113 return $serialized;
114 }
115}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToMany.php b/vendor/doctrine/orm/src/Mapping/ManyToMany.php
new file mode 100644
index 0000000..d90a762
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToMany.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class ManyToMany implements MappingAttribute
11{
12 /**
13 * @param class-string $targetEntity
14 * @param string[]|null $cascade
15 * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
16 */
17 public function __construct(
18 public readonly string $targetEntity,
19 public readonly string|null $mappedBy = null,
20 public readonly string|null $inversedBy = null,
21 public readonly array|null $cascade = null,
22 public readonly string $fetch = 'LAZY',
23 public readonly bool $orphanRemoval = false,
24 public readonly string|null $indexBy = null,
25 ) {
26 }
27}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php
new file mode 100644
index 0000000..8d963c2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7interface ManyToManyAssociationMapping extends ToManyAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php
new file mode 100644
index 0000000..8cd5cbd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7final class ManyToManyInverseSideMapping extends ToManyInverseSideMapping implements ManyToManyAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php
new file mode 100644
index 0000000..b09d56c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php
@@ -0,0 +1,185 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use function strtolower;
8use function trim;
9
10final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implements ManyToManyAssociationMapping
11{
12 /**
13 * Specification of the join table and its join columns (foreign keys).
14 * Only valid for many-to-many mappings. Note that one-to-many associations
15 * can be mapped through a join table by simply mapping the association as
16 * many-to-many with a unique constraint on the join table.
17 */
18 public JoinTableMapping $joinTable;
19
20 /** @var list<mixed> */
21 public array $joinTableColumns = [];
22
23 /** @var array<string, string> */
24 public array $relationToSourceKeyColumns = [];
25 /** @var array<string, string> */
26 public array $relationToTargetKeyColumns = [];
27
28 /** @return array<string, mixed> */
29 public function toArray(): array
30 {
31 $array = parent::toArray();
32
33 $array['joinTable'] = $this->joinTable->toArray();
34
35 return $array;
36 }
37
38 /**
39 * @param mixed[] $mappingArray
40 * @psalm-param array{
41 * fieldName: string,
42 * sourceEntity: class-string,
43 * targetEntity: class-string,
44 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
45 * fetch?: ClassMetadata::FETCH_*|null,
46 * inherited?: class-string|null,
47 * declared?: class-string|null,
48 * cache?: array<mixed>|null,
49 * id?: bool|null,
50 * isOnDeleteCascade?: bool|null,
51 * originalClass?: class-string|null,
52 * originalField?: string|null,
53 * orphanRemoval?: bool,
54 * unique?: bool|null,
55 * joinTable?: mixed[]|null,
56 * type?: int,
57 * isOwningSide: bool,
58 * } $mappingArray
59 */
60 public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self
61 {
62 if (isset($mappingArray['joinTable']['joinColumns'])) {
63 foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) {
64 if (empty($joinColumn['name'])) {
65 $mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
66 $mappingArray['sourceEntity'],
67 $joinColumn['referencedColumnName'] ?? null,
68 );
69 }
70 }
71 }
72
73 if (isset($mappingArray['joinTable']['inverseJoinColumns'])) {
74 foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) {
75 if (empty($joinColumn['name'])) {
76 $mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
77 $mappingArray['targetEntity'],
78 $joinColumn['referencedColumnName'] ?? null,
79 );
80 }
81 }
82 }
83
84 // owning side MUST have a join table
85 if (! isset($mappingArray['joinTable']) || ! isset($mappingArray['joinTable']['name'])) {
86 $mappingArray['joinTable']['name'] = $namingStrategy->joinTableName(
87 $mappingArray['sourceEntity'],
88 $mappingArray['targetEntity'],
89 $mappingArray['fieldName'],
90 );
91 }
92
93 $mapping = parent::fromMappingArray($mappingArray);
94
95 $selfReferencingEntityWithoutJoinColumns = $mapping->sourceEntity === $mapping->targetEntity
96 && $mapping->joinTable->joinColumns === []
97 && $mapping->joinTable->inverseJoinColumns === [];
98
99 if ($mapping->joinTable->joinColumns === []) {
100 $mapping->joinTable->joinColumns = [
101 JoinColumnMapping::fromMappingArray([
102 'name' => $namingStrategy->joinKeyColumnName($mapping->sourceEntity, $selfReferencingEntityWithoutJoinColumns ? 'source' : null),
103 'referencedColumnName' => $namingStrategy->referenceColumnName(),
104 'onDelete' => 'CASCADE',
105 ]),
106 ];
107 }
108
109 if ($mapping->joinTable->inverseJoinColumns === []) {
110 $mapping->joinTable->inverseJoinColumns = [
111 JoinColumnMapping::fromMappingArray([
112 'name' => $namingStrategy->joinKeyColumnName($mapping->targetEntity, $selfReferencingEntityWithoutJoinColumns ? 'target' : null),
113 'referencedColumnName' => $namingStrategy->referenceColumnName(),
114 'onDelete' => 'CASCADE',
115 ]),
116 ];
117 }
118
119 $mapping->joinTableColumns = [];
120
121 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
122 if (empty($joinColumn->referencedColumnName)) {
123 $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
124 }
125
126 if ($joinColumn->name[0] === '`') {
127 $joinColumn->name = trim($joinColumn->name, '`');
128 $joinColumn->quoted = true;
129 }
130
131 if ($joinColumn->referencedColumnName[0] === '`') {
132 $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`');
133 $joinColumn->quoted = true;
134 }
135
136 if (isset($joinColumn->onDelete) && strtolower($joinColumn->onDelete) === 'cascade') {
137 $mapping->isOnDeleteCascade = true;
138 }
139
140 $mapping->relationToSourceKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName;
141 $mapping->joinTableColumns[] = $joinColumn->name;
142 }
143
144 foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
145 if (empty($inverseJoinColumn->referencedColumnName)) {
146 $inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
147 }
148
149 if ($inverseJoinColumn->name[0] === '`') {
150 $inverseJoinColumn->name = trim($inverseJoinColumn->name, '`');
151 $inverseJoinColumn->quoted = true;
152 }
153
154 if ($inverseJoinColumn->referencedColumnName[0] === '`') {
155 $inverseJoinColumn->referencedColumnName = trim($inverseJoinColumn->referencedColumnName, '`');
156 $inverseJoinColumn->quoted = true;
157 }
158
159 if (isset($inverseJoinColumn->onDelete) && strtolower($inverseJoinColumn->onDelete) === 'cascade') {
160 $mapping->isOnDeleteCascade = true;
161 }
162
163 $mapping->relationToTargetKeyColumns[$inverseJoinColumn->name] = $inverseJoinColumn->referencedColumnName;
164 $mapping->joinTableColumns[] = $inverseJoinColumn->name;
165 }
166
167 return $mapping;
168 }
169
170 /** @return list<string> */
171 public function __sleep(): array
172 {
173 $serialized = parent::__sleep();
174 $serialized[] = 'joinTable';
175 $serialized[] = 'joinTableColumns';
176
177 foreach (['relationToSourceKeyColumns', 'relationToTargetKeyColumns'] as $arrayKey) {
178 if ($this->$arrayKey !== null) {
179 $serialized[] = $arrayKey;
180 }
181 }
182
183 return $serialized;
184 }
185}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToOne.php b/vendor/doctrine/orm/src/Mapping/ManyToOne.php
new file mode 100644
index 0000000..8fccff3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToOne.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class ManyToOne implements MappingAttribute
11{
12 /**
13 * @param class-string|null $targetEntity
14 * @param string[]|null $cascade
15 * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
16 */
17 public function __construct(
18 public readonly string|null $targetEntity = null,
19 public readonly array|null $cascade = null,
20 public readonly string $fetch = 'LAZY',
21 public readonly string|null $inversedBy = null,
22 ) {
23 }
24}
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php
new file mode 100644
index 0000000..3d1bf90
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/**
8 * The "many" side of a many-to-one association mapping is always the owning side.
9 */
10final class ManyToOneAssociationMapping extends ToOneOwningSideMapping
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php b/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php
new file mode 100644
index 0000000..29475a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8use Doctrine\ORM\EntityRepository;
9
10#[Attribute(Attribute::TARGET_CLASS)]
11final class MappedSuperclass implements MappingAttribute
12{
13 /** @psalm-param class-string<EntityRepository>|null $repositoryClass */
14 public function __construct(
15 public readonly string|null $repositoryClass = null,
16 ) {
17 }
18}
diff --git a/vendor/doctrine/orm/src/Mapping/MappingAttribute.php b/vendor/doctrine/orm/src/Mapping/MappingAttribute.php
new file mode 100644
index 0000000..61091a9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/MappingAttribute.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/** A marker interface for mapping attributes. */
8interface MappingAttribute
9{
10}
diff --git a/vendor/doctrine/orm/src/Mapping/MappingException.php b/vendor/doctrine/orm/src/Mapping/MappingException.php
new file mode 100644
index 0000000..9b73242
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/MappingException.php
@@ -0,0 +1,691 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use BackedEnum;
8use Doctrine\ORM\Exception\ORMException;
9use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException;
10use LibXMLError;
11use ReflectionException;
12use ValueError;
13
14use function array_keys;
15use function array_map;
16use function array_values;
17use function get_debug_type;
18use function get_parent_class;
19use function implode;
20use function sprintf;
21
22use const PHP_EOL;
23
24/**
25 * A MappingException indicates that something is wrong with the mapping setup.
26 */
27class MappingException extends PersistenceMappingException implements ORMException
28{
29 /** @param class-string $entityName */
30 public static function identifierRequired(string $entityName): self
31 {
32 $parent = get_parent_class($entityName);
33 if ($parent !== false) {
34 return new self(sprintf(
35 'No identifier/primary key specified for Entity "%s" sub class of "%s". Every Entity must have an identifier/primary key.',
36 $entityName,
37 $parent,
38 ));
39 }
40
41 return new self(sprintf(
42 'No identifier/primary key specified for Entity "%s". Every Entity must have an identifier/primary key.',
43 $entityName,
44 ));
45 }
46
47 public static function invalidAssociationType(string $entityName, string $fieldName, int $type): self
48 {
49 return new self(sprintf(
50 'The association "%s#%s" must be of type "ClassMetadata::ONE_TO_MANY", "ClassMetadata::MANY_TO_MANY" or "ClassMetadata::MANY_TO_ONE", "%d" given.',
51 $entityName,
52 $fieldName,
53 $type,
54 ));
55 }
56
57 public static function invalidInheritanceType(string $entityName, int $type): self
58 {
59 return new self(sprintf("The inheritance type '%s' specified for '%s' does not exist.", $type, $entityName));
60 }
61
62 public static function generatorNotAllowedWithCompositeId(): self
63 {
64 return new self("Id generators can't be used with a composite id.");
65 }
66
67 public static function missingFieldName(string $entity): self
68 {
69 return new self(sprintf(
70 "The field or association mapping misses the 'fieldName' attribute in entity '%s'.",
71 $entity,
72 ));
73 }
74
75 public static function missingTargetEntity(string $fieldName): self
76 {
77 return new self(sprintf("The association mapping '%s' misses the 'targetEntity' attribute.", $fieldName));
78 }
79
80 public static function missingSourceEntity(string $fieldName): self
81 {
82 return new self(sprintf("The association mapping '%s' misses the 'sourceEntity' attribute.", $fieldName));
83 }
84
85 public static function missingEmbeddedClass(string $fieldName): self
86 {
87 return new self(sprintf("The embed mapping '%s' misses the 'class' attribute.", $fieldName));
88 }
89
90 public static function mappingFileNotFound(string $entityName, string $fileName): self
91 {
92 return new self(sprintf("No mapping file found named '%s' for class '%s'.", $fileName, $entityName));
93 }
94
95 /**
96 * Exception for invalid property name override.
97 *
98 * @param string $className The entity's name.
99 */
100 public static function invalidOverrideFieldName(string $className, string $fieldName): self
101 {
102 return new self(sprintf("Invalid field override named '%s' for class '%s'.", $fieldName, $className));
103 }
104
105 /**
106 * Exception for invalid property type override.
107 *
108 * @param string $className The entity's name.
109 */
110 public static function invalidOverrideFieldType(string $className, string $fieldName): self
111 {
112 return new self(sprintf(
113 "The column type of attribute '%s' on class '%s' could not be changed.",
114 $fieldName,
115 $className,
116 ));
117 }
118
119 public static function mappingNotFound(string $className, string $fieldName): self
120 {
121 return new self(sprintf("No mapping found for field '%s' on class '%s'.", $fieldName, $className));
122 }
123
124 public static function queryNotFound(string $className, string $queryName): self
125 {
126 return new self(sprintf("No query found named '%s' on class '%s'.", $queryName, $className));
127 }
128
129 public static function resultMappingNotFound(string $className, string $resultName): self
130 {
131 return new self(sprintf("No result set mapping found named '%s' on class '%s'.", $resultName, $className));
132 }
133
134 public static function emptyQueryMapping(string $entity, string $queryName): self
135 {
136 return new self(sprintf('Query named "%s" in "%s" could not be empty.', $queryName, $entity));
137 }
138
139 public static function nameIsMandatoryForQueryMapping(string $className): self
140 {
141 return new self(sprintf("Query name on entity class '%s' is not defined.", $className));
142 }
143
144 public static function missingQueryMapping(string $entity, string $queryName): self
145 {
146 return new self(sprintf(
147 'Query named "%s" in "%s requires a result class or result set mapping.',
148 $queryName,
149 $entity,
150 ));
151 }
152
153 public static function missingResultSetMappingEntity(string $entity, string $resultName): self
154 {
155 return new self(sprintf(
156 'Result set mapping named "%s" in "%s requires a entity class name.',
157 $resultName,
158 $entity,
159 ));
160 }
161
162 public static function missingResultSetMappingFieldName(string $entity, string $resultName): self
163 {
164 return new self(sprintf(
165 'Result set mapping named "%s" in "%s requires a field name.',
166 $resultName,
167 $entity,
168 ));
169 }
170
171 public static function oneToManyRequiresMappedBy(string $entityName, string $fieldName): MappingException
172 {
173 return new self(sprintf(
174 "OneToMany mapping on entity '%s' field '%s' requires the 'mappedBy' attribute.",
175 $entityName,
176 $fieldName,
177 ));
178 }
179
180 public static function joinTableRequired(string $fieldName): self
181 {
182 return new self(sprintf("The mapping of field '%s' requires an the 'joinTable' attribute.", $fieldName));
183 }
184
185 /**
186 * Called if a required option was not found but is required
187 *
188 * @param string $field Which field cannot be processed?
189 * @param string $expectedOption Which option is required
190 * @param string $hint Can optionally be used to supply a tip for common mistakes,
191 * e.g. "Did you think of the plural s?"
192 */
193 public static function missingRequiredOption(string $field, string $expectedOption, string $hint = ''): self
194 {
195 $message = "The mapping of field '" . $field . "' is invalid: The option '" . $expectedOption . "' is required.";
196
197 if (! empty($hint)) {
198 $message .= ' (Hint: ' . $hint . ')';
199 }
200
201 return new self($message);
202 }
203
204 /**
205 * Generic exception for invalid mappings.
206 */
207 public static function invalidMapping(string $fieldName): self
208 {
209 return new self(sprintf("The mapping of field '%s' is invalid.", $fieldName));
210 }
211
212 /**
213 * Exception for reflection exceptions - adds the entity name,
214 * because there might be long classnames that will be shortened
215 * within the stacktrace
216 *
217 * @param string $entity The entity's name
218 */
219 public static function reflectionFailure(string $entity, ReflectionException $previousException): self
220 {
221 return new self(sprintf('An error occurred in %s', $entity), 0, $previousException);
222 }
223
224 public static function joinColumnMustPointToMappedField(string $className, string $joinColumn): self
225 {
226 return new self(sprintf(
227 'The column %s must be mapped to a field in class %s since it is referenced by a join column of another class.',
228 $joinColumn,
229 $className,
230 ));
231 }
232
233 public static function joinColumnNotAllowedOnOneToOneInverseSide(string $className, string $fieldName): self
234 {
235 return new self(sprintf(
236 '%s#%s is a OneToOne inverse side, which does not allow join columns.',
237 $className,
238 $fieldName,
239 ));
240 }
241
242 /** @param class-string $className */
243 public static function classIsNotAValidEntityOrMappedSuperClass(string $className): self
244 {
245 $parent = get_parent_class($className);
246 if ($parent !== false) {
247 return new self(sprintf(
248 'Class "%s" sub class of "%s" is not a valid entity or mapped super class.',
249 $className,
250 $parent,
251 ));
252 }
253
254 return new self(sprintf(
255 'Class "%s" is not a valid entity or mapped super class.',
256 $className,
257 ));
258 }
259
260 /**
261 * @param string $entity The entity's name.
262 * @param string $fieldName The name of the field that was already declared.
263 */
264 public static function duplicateFieldMapping(string $entity, string $fieldName): self
265 {
266 return new self(sprintf(
267 'Property "%s" in "%s" was already declared, but it must be declared only once',
268 $fieldName,
269 $entity,
270 ));
271 }
272
273 public static function duplicateAssociationMapping(string $entity, string $fieldName): self
274 {
275 return new self(sprintf(
276 'Property "%s" in "%s" was already declared, but it must be declared only once',
277 $fieldName,
278 $entity,
279 ));
280 }
281
282 public static function duplicateQueryMapping(string $entity, string $queryName): self
283 {
284 return new self(sprintf(
285 'Query named "%s" in "%s" was already declared, but it must be declared only once',
286 $queryName,
287 $entity,
288 ));
289 }
290
291 public static function duplicateResultSetMapping(string $entity, string $resultName): self
292 {
293 return new self(sprintf(
294 'Result set mapping named "%s" in "%s" was already declared, but it must be declared only once',
295 $resultName,
296 $entity,
297 ));
298 }
299
300 public static function singleIdNotAllowedOnCompositePrimaryKey(string $entity): self
301 {
302 return new self('Single id is not allowed on composite primary key in entity ' . $entity);
303 }
304
305 public static function noIdDefined(string $entity): self
306 {
307 return new self('No ID defined for entity ' . $entity);
308 }
309
310 public static function unsupportedOptimisticLockingType(string $entity, string $fieldName, string $unsupportedType): self
311 {
312 return new self(sprintf(
313 'Locking type "%s" (specified in "%s", field "%s") is not supported by Doctrine.',
314 $unsupportedType,
315 $entity,
316 $fieldName,
317 ));
318 }
319
320 public static function fileMappingDriversRequireConfiguredDirectoryPath(string|null $path = null): self
321 {
322 if (! empty($path)) {
323 $path = '[' . $path . ']';
324 }
325
326 return new self(
327 'File mapping drivers must have a valid directory path, ' .
328 'however the given path ' . $path . ' seems to be incorrect!',
329 );
330 }
331
332 /**
333 * Returns an exception that indicates that a class used in a discriminator map does not exist.
334 * An example would be an outdated (maybe renamed) classname.
335 *
336 * @param string $className The class that could not be found
337 * @param string $owningClass The class that declares the discriminator map.
338 */
339 public static function invalidClassInDiscriminatorMap(string $className, string $owningClass): self
340 {
341 return new self(sprintf(
342 "Entity class '%s' used in the discriminator map of class '%s' " .
343 'does not exist.',
344 $className,
345 $owningClass,
346 ));
347 }
348
349 /**
350 * @param string[] $entries
351 * @param array<string,string> $map
352 */
353 public static function duplicateDiscriminatorEntry(string $className, array $entries, array $map): self
354 {
355 return new self(
356 'The entries ' . implode(', ', $entries) . " in discriminator map of class '" . $className . "' is duplicated. " .
357 'If the discriminator map is automatically generated you have to convert it to an explicit discriminator map now. ' .
358 'The entries of the current map are: @DiscriminatorMap({' . implode(', ', array_map(
359 static fn ($a, $b) => sprintf("'%s': '%s'", $a, $b),
360 array_keys($map),
361 array_values($map),
362 )) . '})',
363 );
364 }
365
366 /**
367 * @param class-string $rootEntityClass
368 * @param class-string $childEntityClass
369 */
370 public static function missingInheritanceTypeDeclaration(string $rootEntityClass, string $childEntityClass): self
371 {
372 return new self(sprintf(
373 "Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.",
374 $childEntityClass,
375 $rootEntityClass,
376 ));
377 }
378
379 public static function missingDiscriminatorMap(string $className): self
380 {
381 return new self(sprintf(
382 "Entity class '%s' is using inheritance but no discriminator map was defined.",
383 $className,
384 ));
385 }
386
387 public static function missingDiscriminatorColumn(string $className): self
388 {
389 return new self(sprintf(
390 "Entity class '%s' is using inheritance but no discriminator column was defined.",
391 $className,
392 ));
393 }
394
395 public static function invalidDiscriminatorColumnType(string $className, string $type): self
396 {
397 return new self(sprintf(
398 "Discriminator column type on entity class '%s' is not allowed to be '%s'. 'string' or 'integer' type variables are suggested!",
399 $className,
400 $type,
401 ));
402 }
403
404 public static function nameIsMandatoryForDiscriminatorColumns(string $className): self
405 {
406 return new self(sprintf("Discriminator column name on entity class '%s' is not defined.", $className));
407 }
408
409 public static function cannotVersionIdField(string $className, string $fieldName): self
410 {
411 return new self(sprintf(
412 "Setting Id field '%s' as versionable in entity class '%s' is not supported.",
413 $fieldName,
414 $className,
415 ));
416 }
417
418 public static function duplicateColumnName(string $className, string $columnName): self
419 {
420 return new self("Duplicate definition of column '" . $columnName . "' on entity '" . $className . "' in a field or discriminator column mapping.");
421 }
422
423 public static function illegalToManyAssociationOnMappedSuperclass(string $className, string $field): self
424 {
425 return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '" . $className . '#' . $field . "'.");
426 }
427
428 public static function cannotMapCompositePrimaryKeyEntitiesAsForeignId(string $className, string $targetEntity, string $targetField): self
429 {
430 return new self("It is not possible to map entity '" . $className . "' with a composite primary key " .
431 "as part of the primary key of another entity '" . $targetEntity . '#' . $targetField . "'.");
432 }
433
434 public static function noSingleAssociationJoinColumnFound(string $className, string $field): self
435 {
436 return new self(sprintf("'%s#%s' is not an association with a single join column.", $className, $field));
437 }
438
439 public static function noFieldNameFoundForColumn(string $className, string $column): self
440 {
441 return new self(sprintf(
442 "Cannot find a field on '%s' that is mapped to column '%s'. Either the " .
443 'field does not exist or an association exists but it has multiple join columns.',
444 $className,
445 $column,
446 ));
447 }
448
449 public static function illegalOrphanRemovalOnIdentifierAssociation(string $className, string $field): self
450 {
451 return new self(sprintf(
452 "The orphan removal option is not allowed on an association that is part of the identifier in '%s#%s'.",
453 $className,
454 $field,
455 ));
456 }
457
458 public static function illegalOrphanRemoval(string $className, string $field): self
459 {
460 return new self('Orphan removal is only allowed on one-to-one and one-to-many ' .
461 'associations, but ' . $className . '#' . $field . ' is not.');
462 }
463
464 public static function illegalInverseIdentifierAssociation(string $className, string $field): self
465 {
466 return new self(sprintf(
467 "An inverse association is not allowed to be identifier in '%s#%s'.",
468 $className,
469 $field,
470 ));
471 }
472
473 public static function illegalToManyIdentifierAssociation(string $className, string $field): self
474 {
475 return new self(sprintf(
476 "Many-to-many or one-to-many associations are not allowed to be identifier in '%s#%s'.",
477 $className,
478 $field,
479 ));
480 }
481
482 public static function noInheritanceOnMappedSuperClass(string $className): self
483 {
484 return new self("It is not supported to define inheritance information on a mapped superclass '" . $className . "'.");
485 }
486
487 public static function mappedClassNotPartOfDiscriminatorMap(string $className, string $rootClassName): self
488 {
489 return new self(
490 "Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " .
491 "to be properly mapped in the inheritance hierarchy. Alternatively you can make '" . $className . "' an abstract class " .
492 'to avoid this exception from occurring.',
493 );
494 }
495
496 public static function lifecycleCallbackMethodNotFound(string $className, string $methodName): self
497 {
498 return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback.");
499 }
500
501 /** @param class-string $className */
502 public static function illegalLifecycleCallbackOnEmbeddedClass(string $event, string $className): self
503 {
504 return new self(sprintf(
505 <<<'EXCEPTION'
506 Context: Attempt to register lifecycle callback "%s" on embedded class "%s".
507 Problem: Registering lifecycle callbacks on embedded classes is not allowed.
508 EXCEPTION,
509 $event,
510 $className,
511 ));
512 }
513
514 public static function entityListenerClassNotFound(string $listenerName, string $className): self
515 {
516 return new self(sprintf('Entity Listener "%s" declared on "%s" not found.', $listenerName, $className));
517 }
518
519 public static function entityListenerMethodNotFound(string $listenerName, string $methodName, string $className): self
520 {
521 return new self(sprintf('Entity Listener "%s" declared on "%s" has no method "%s".', $listenerName, $className, $methodName));
522 }
523
524 public static function duplicateEntityListener(string $listenerName, string $methodName, string $className): self
525 {
526 return new self(sprintf('Entity Listener "%s#%s()" in "%s" was already declared, but it must be declared only once.', $listenerName, $methodName, $className));
527 }
528
529 /** @param class-string $className */
530 public static function invalidFetchMode(string $className, string $fetchMode): self
531 {
532 return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $fetchMode . "'");
533 }
534
535 public static function invalidGeneratedMode(int|string $generatedMode): self
536 {
537 return new self("Invalid generated mode '" . $generatedMode . "'");
538 }
539
540 public static function compositeKeyAssignedIdGeneratorRequired(string $className): self
541 {
542 return new self("Entity '" . $className . "' has a composite identifier but uses an ID generator other than manually assigning (Identity, Sequence). This is not supported.");
543 }
544
545 public static function invalidTargetEntityClass(string $targetEntity, string $sourceEntity, string $associationName): self
546 {
547 return new self('The target-entity ' . $targetEntity . " cannot be found in '" . $sourceEntity . '#' . $associationName . "'.");
548 }
549
550 /** @param string[] $cascades */
551 public static function invalidCascadeOption(array $cascades, string $className, string $propertyName): self
552 {
553 $cascades = implode(', ', array_map(static fn (string $e): string => "'" . $e . "'", $cascades));
554
555 return new self(sprintf(
556 "You have specified invalid cascade options for %s::$%s: %s; available options: 'remove', 'persist', 'refresh', and 'detach'",
557 $className,
558 $propertyName,
559 $cascades,
560 ));
561 }
562
563 public static function missingSequenceName(string $className): self
564 {
565 return new self(
566 sprintf('Missing "sequenceName" attribute for sequence id generator definition on class "%s".', $className),
567 );
568 }
569
570 public static function infiniteEmbeddableNesting(string $className, string $propertyName): self
571 {
572 return new self(
573 sprintf(
574 'Infinite nesting detected for embedded property %s::%s. ' .
575 'You cannot embed an embeddable from the same type inside an embeddable.',
576 $className,
577 $propertyName,
578 ),
579 );
580 }
581
582 public static function illegalOverrideOfInheritedProperty(string $className, string $propertyName, string $inheritFromClass): self
583 {
584 return new self(
585 sprintf(
586 'Overrides are only allowed for fields or associations declared in mapped superclasses or traits. This is not the case for %s::%s, which was inherited from %s.',
587 $className,
588 $propertyName,
589 $inheritFromClass,
590 ),
591 );
592 }
593
594 public static function invalidIndexConfiguration(string $className, string $indexName): self
595 {
596 return new self(
597 sprintf(
598 'Index %s for entity %s should contain columns or fields values, but not both.',
599 $indexName,
600 $className,
601 ),
602 );
603 }
604
605 public static function invalidUniqueConstraintConfiguration(string $className, string $indexName): self
606 {
607 return new self(
608 sprintf(
609 'Unique constraint %s for entity %s should contain columns or fields values, but not both.',
610 $indexName,
611 $className,
612 ),
613 );
614 }
615
616 public static function invalidOverrideType(string $expectdType, mixed $givenValue): self
617 {
618 return new self(sprintf(
619 'Expected %s, but %s was given.',
620 $expectdType,
621 get_debug_type($givenValue),
622 ));
623 }
624
625 public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
626 {
627 return new self(sprintf(
628 'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
629 $enumType,
630 $className,
631 $fieldName,
632 ));
633 }
634
635 public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
636 {
637 return new self(sprintf(
638 'Attempting to map non-enum type %s as enum in entity %s::$%s',
639 $enumType,
640 $className,
641 $fieldName,
642 ));
643 }
644
645 /**
646 * @param class-string $className
647 * @param class-string<BackedEnum> $enumType
648 */
649 public static function invalidEnumValue(
650 string $className,
651 string $fieldName,
652 string $value,
653 string $enumType,
654 ValueError $previous,
655 ): self {
656 return new self(sprintf(
657 <<<'EXCEPTION'
658Context: Trying to hydrate enum property "%s::$%s"
659Problem: Case "%s" is not listed in enum "%s"
660Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
661EXCEPTION
662 ,
663 $className,
664 $fieldName,
665 $value,
666 $enumType,
667 ), 0, $previous);
668 }
669
670 /** @param LibXMLError[] $errors */
671 public static function fromLibXmlErrors(array $errors): self
672 {
673 $formatter = static fn (LibXMLError $error): string => sprintf(
674 'libxml error: %s in %s at line %d',
675 $error->message,
676 $error->file,
677 $error->line,
678 );
679
680 return new self(implode(PHP_EOL, array_map($formatter, $errors)));
681 }
682
683 public static function invalidAttributeOnEmbeddable(string $entityName, string $attributeName): self
684 {
685 return new self(sprintf(
686 'Attribute "%s" on embeddable "%s" is not allowed.',
687 $attributeName,
688 $entityName,
689 ));
690 }
691}
diff --git a/vendor/doctrine/orm/src/Mapping/NamingStrategy.php b/vendor/doctrine/orm/src/Mapping/NamingStrategy.php
new file mode 100644
index 0000000..afedebe
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/NamingStrategy.php
@@ -0,0 +1,71 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7/**
8 * A set of rules for determining the physical column and table names
9 *
10 * @link www.doctrine-project.org
11 */
12interface NamingStrategy
13{
14 /**
15 * Returns a table name for an entity class.
16 *
17 * @param class-string $className
18 */
19 public function classToTableName(string $className): string;
20
21 /**
22 * Returns a column name for a property.
23 *
24 * @param class-string $className
25 */
26 public function propertyToColumnName(string $propertyName, string $className): string;
27
28 /**
29 * Returns a column name for an embedded property.
30 *
31 * @param class-string $className
32 * @param class-string $embeddedClassName
33 */
34 public function embeddedFieldToColumnName(
35 string $propertyName,
36 string $embeddedColumnName,
37 string $className,
38 string $embeddedClassName,
39 ): string;
40
41 /**
42 * Returns the default reference column name.
43 */
44 public function referenceColumnName(): string;
45
46 /**
47 * Returns a join column name for a property.
48 *
49 * @param class-string $className
50 */
51 public function joinColumnName(string $propertyName, string $className): string;
52
53 /**
54 * Returns a join table name.
55 *
56 * @param class-string $sourceEntity
57 * @param class-string $targetEntity
58 */
59 public function joinTableName(string $sourceEntity, string $targetEntity, string $propertyName): string;
60
61 /**
62 * Returns the foreign key column name for the given parameters.
63 *
64 * @param class-string $entityName An entity.
65 * @param string|null $referencedColumnName A property name or null in
66 * case of a self-referencing
67 * entity with join columns
68 * defined in the mapping
69 */
70 public function joinKeyColumnName(string $entityName, string|null $referencedColumnName): string;
71}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToMany.php b/vendor/doctrine/orm/src/Mapping/OneToMany.php
new file mode 100644
index 0000000..d71c4f9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToMany.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class OneToMany implements MappingAttribute
11{
12 /**
13 * @param class-string|null $targetEntity
14 * @param string[]|null $cascade
15 * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
16 */
17 public function __construct(
18 public readonly string|null $targetEntity = null,
19 public readonly string|null $mappedBy = null,
20 public readonly array|null $cascade = null,
21 public readonly string $fetch = 'LAZY',
22 public readonly bool $orphanRemoval = false,
23 public readonly string|null $indexBy = null,
24 ) {
25 }
26}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php
new file mode 100644
index 0000000..786e981
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php
@@ -0,0 +1,75 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7final class OneToManyAssociationMapping extends ToManyInverseSideMapping
8{
9 /**
10 * @param mixed[] $mappingArray
11 * @psalm-param array{
12 * fieldName: string,
13 * sourceEntity: class-string,
14 * targetEntity: class-string,
15 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
16 * fetch?: ClassMetadata::FETCH_*|null,
17 * inherited?: class-string|null,
18 * declared?: class-string|null,
19 * cache?: array<mixed>|null,
20 * id?: bool|null,
21 * isOnDeleteCascade?: bool|null,
22 * originalClass?: class-string|null,
23 * originalField?: string|null,
24 * orphanRemoval?: bool,
25 * unique?: bool|null,
26 * joinTable?: mixed[]|null,
27 * type?: int,
28 * isOwningSide: bool,
29 * } $mappingArray
30 */
31 public static function fromMappingArray(array $mappingArray): static
32 {
33 $mapping = parent::fromMappingArray($mappingArray);
34
35 if ($mapping->orphanRemoval && ! $mapping->isCascadeRemove()) {
36 $mapping->cascade[] = 'remove';
37 }
38
39 return $mapping;
40 }
41
42 /**
43 * @param mixed[] $mappingArray
44 * @psalm-param array{
45 * fieldName: string,
46 * sourceEntity: class-string,
47 * targetEntity: class-string,
48 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
49 * fetch?: ClassMetadata::FETCH_*|null,
50 * inherited?: class-string|null,
51 * declared?: class-string|null,
52 * cache?: array<mixed>|null,
53 * id?: bool|null,
54 * isOnDeleteCascade?: bool|null,
55 * originalClass?: class-string|null,
56 * originalField?: string|null,
57 * orphanRemoval?: bool,
58 * unique?: bool|null,
59 * joinTable?: mixed[]|null,
60 * type?: int,
61 * isOwningSide: bool,
62 * } $mappingArray
63 */
64 public static function fromMappingArrayAndName(array $mappingArray, string $name): static
65 {
66 $mapping = self::fromMappingArray($mappingArray);
67
68 // OneToMany-side MUST be inverse (must have mappedBy)
69 if (! isset($mapping->mappedBy)) {
70 throw MappingException::oneToManyRequiresMappedBy($name, $mapping->fieldName);
71 }
72
73 return $mapping;
74 }
75}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOne.php b/vendor/doctrine/orm/src/Mapping/OneToOne.php
new file mode 100644
index 0000000..1ddf21c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToOne.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class OneToOne implements MappingAttribute
11{
12 /**
13 * @param class-string|null $targetEntity
14 * @param array<string>|null $cascade
15 * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
16 */
17 public function __construct(
18 public readonly string|null $targetEntity = null,
19 public readonly string|null $mappedBy = null,
20 public readonly string|null $inversedBy = null,
21 public readonly array|null $cascade = null,
22 public readonly string $fetch = 'LAZY',
23 public readonly bool $orphanRemoval = false,
24 ) {
25 }
26}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php
new file mode 100644
index 0000000..89c6483
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7interface OneToOneAssociationMapping extends ToOneAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php
new file mode 100644
index 0000000..85e0f30
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7final class OneToOneInverseSideMapping extends ToOneInverseSideMapping implements OneToOneAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php
new file mode 100644
index 0000000..4ad181b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7final class OneToOneOwningSideMapping extends ToOneOwningSideMapping implements OneToOneAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/OrderBy.php b/vendor/doctrine/orm/src/Mapping/OrderBy.php
new file mode 100644
index 0000000..5cb2ed9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OrderBy.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class OrderBy implements MappingAttribute
11{
12 /** @param array<string> $value */
13 public function __construct(
14 public readonly array $value,
15 ) {
16 }
17}
diff --git a/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php
new file mode 100644
index 0000000..ab8b7b2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7abstract class OwningSideMapping extends AssociationMapping
8{
9 /**
10 * required for bidirectional associations
11 * The name of the field that completes the bidirectional association on
12 * the inverse side. This key must be specified on the owning side of a
13 * bidirectional association.
14 */
15 public string|null $inversedBy = null;
16
17 /** @return list<string> */
18 public function __sleep(): array
19 {
20 $serialized = parent::__sleep();
21
22 if ($this->inversedBy !== null) {
23 $serialized[] = 'inversedBy';
24 }
25
26 return $serialized;
27 }
28}
diff --git a/vendor/doctrine/orm/src/Mapping/PostLoad.php b/vendor/doctrine/orm/src/Mapping/PostLoad.php
new file mode 100644
index 0000000..9ce1e5c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PostLoad.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PostLoad implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PostPersist.php b/vendor/doctrine/orm/src/Mapping/PostPersist.php
new file mode 100644
index 0000000..f7bf2e1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PostPersist.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PostPersist implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PostRemove.php b/vendor/doctrine/orm/src/Mapping/PostRemove.php
new file mode 100644
index 0000000..394c175
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PostRemove.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PostRemove implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PostUpdate.php b/vendor/doctrine/orm/src/Mapping/PostUpdate.php
new file mode 100644
index 0000000..7b95675
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PostUpdate.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PostUpdate implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PreFlush.php b/vendor/doctrine/orm/src/Mapping/PreFlush.php
new file mode 100644
index 0000000..f2c09d7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PreFlush.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PreFlush implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PrePersist.php b/vendor/doctrine/orm/src/Mapping/PrePersist.php
new file mode 100644
index 0000000..1b88a7a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PrePersist.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PrePersist implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PreRemove.php b/vendor/doctrine/orm/src/Mapping/PreRemove.php
new file mode 100644
index 0000000..f63d4e0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PreRemove.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PreRemove implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/PreUpdate.php b/vendor/doctrine/orm/src/Mapping/PreUpdate.php
new file mode 100644
index 0000000..9b73bfd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/PreUpdate.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_METHOD)]
10final class PreUpdate implements MappingAttribute
11{
12}
diff --git a/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php
new file mode 100644
index 0000000..9eb3e53
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php
@@ -0,0 +1,68 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9/**
10 * A set of rules for determining the column, alias and table quotes.
11 */
12interface QuoteStrategy
13{
14 /**
15 * Gets the (possibly quoted) column name for safe use in an SQL statement.
16 */
17 public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string;
18
19 /**
20 * Gets the (possibly quoted) primary table name for safe use in an SQL statement.
21 */
22 public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string;
23
24 /**
25 * Gets the (possibly quoted) sequence name for safe use in an SQL statement.
26 *
27 * @param mixed[] $definition
28 */
29 public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string;
30
31 /** Gets the (possibly quoted) name of the join table. */
32 public function getJoinTableName(
33 ManyToManyOwningSideMapping $association,
34 ClassMetadata $class,
35 AbstractPlatform $platform,
36 ): string;
37
38 /**
39 * Gets the (possibly quoted) join column name.
40 */
41 public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string;
42
43 /**
44 * Gets the (possibly quoted) join column name.
45 */
46 public function getReferencedJoinColumnName(
47 JoinColumnMapping $joinColumn,
48 ClassMetadata $class,
49 AbstractPlatform $platform,
50 ): string;
51
52 /**
53 * Gets the (possibly quoted) identifier column names for safe use in an SQL statement.
54 *
55 * @psalm-return list<string>
56 */
57 public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array;
58
59 /**
60 * Gets the column alias.
61 */
62 public function getColumnAlias(
63 string $columnName,
64 int $counter,
65 AbstractPlatform $platform,
66 ClassMetadata|null $class = null,
67 ): string;
68}
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php
new file mode 100644
index 0000000..da3d097
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php
@@ -0,0 +1,61 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Doctrine\Instantiator\Instantiator;
8use ReflectionProperty;
9
10/**
11 * Acts as a proxy to a nested Property structure, making it look like
12 * just a single scalar property.
13 *
14 * This way value objects "just work" without UnitOfWork, Persisters or Hydrators
15 * needing any changes.
16 *
17 * TODO: Move this class into Common\Reflection
18 */
19final class ReflectionEmbeddedProperty extends ReflectionProperty
20{
21 private Instantiator|null $instantiator = null;
22
23 /**
24 * @param ReflectionProperty $parentProperty reflection property of the class where the embedded object has to be put
25 * @param ReflectionProperty $childProperty reflection property of the embedded object
26 * @psalm-param class-string $embeddedClass
27 */
28 public function __construct(
29 private readonly ReflectionProperty $parentProperty,
30 private readonly ReflectionProperty $childProperty,
31 private readonly string $embeddedClass,
32 ) {
33 parent::__construct($childProperty->getDeclaringClass()->name, $childProperty->getName());
34 }
35
36 public function getValue(object|null $object = null): mixed
37 {
38 $embeddedObject = $this->parentProperty->getValue($object);
39
40 if ($embeddedObject === null) {
41 return null;
42 }
43
44 return $this->childProperty->getValue($embeddedObject);
45 }
46
47 public function setValue(mixed $object, mixed $value = null): void
48 {
49 $embeddedObject = $this->parentProperty->getValue($object);
50
51 if ($embeddedObject === null) {
52 $this->instantiator ??= new Instantiator();
53
54 $embeddedObject = $this->instantiator->instantiate($this->embeddedClass);
55
56 $this->parentProperty->setValue($object, $embeddedObject);
57 }
58
59 $this->childProperty->setValue($embeddedObject, $value);
60 }
61}
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php
new file mode 100644
index 0000000..0ebd978
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php
@@ -0,0 +1,87 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use BackedEnum;
8use ReflectionProperty;
9use ValueError;
10
11use function array_map;
12use function is_array;
13
14/** @deprecated use Doctrine\Persistence\Reflection\EnumReflectionProperty instead */
15final class ReflectionEnumProperty extends ReflectionProperty
16{
17 /** @param class-string<BackedEnum> $enumType */
18 public function __construct(
19 private readonly ReflectionProperty $originalReflectionProperty,
20 private readonly string $enumType,
21 ) {
22 parent::__construct(
23 $originalReflectionProperty->class,
24 $originalReflectionProperty->name,
25 );
26 }
27
28 public function getValue(object|null $object = null): int|string|array|null
29 {
30 if ($object === null) {
31 return null;
32 }
33
34 $enum = $this->originalReflectionProperty->getValue($object);
35
36 if ($enum === null) {
37 return null;
38 }
39
40 if (is_array($enum)) {
41 return array_map(
42 static fn (BackedEnum $item): int|string => $item->value,
43 $enum,
44 );
45 }
46
47 return $enum->value;
48 }
49
50 /**
51 * @param object $object
52 * @param int|string|int[]|string[]|BackedEnum|BackedEnum[]|null $value
53 */
54 public function setValue(mixed $object, mixed $value = null): void
55 {
56 if ($value !== null) {
57 if (is_array($value)) {
58 $value = array_map(fn (int|string|BackedEnum $item): BackedEnum => $this->initializeEnumValue($object, $item), $value);
59 } else {
60 $value = $this->initializeEnumValue($object, $value);
61 }
62 }
63
64 $this->originalReflectionProperty->setValue($object, $value);
65 }
66
67 private function initializeEnumValue(object $object, int|string|BackedEnum $value): BackedEnum
68 {
69 if ($value instanceof BackedEnum) {
70 return $value;
71 }
72
73 $enumType = $this->enumType;
74
75 try {
76 return $enumType::from($value);
77 } catch (ValueError $e) {
78 throw MappingException::invalidEnumValue(
79 $object::class,
80 $this->originalReflectionProperty->name,
81 (string) $value,
82 $enumType,
83 $e,
84 );
85 }
86 }
87}
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php
new file mode 100644
index 0000000..13e9f6d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php
@@ -0,0 +1,49 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use InvalidArgumentException;
8use LogicException;
9use ReflectionProperty;
10
11use function assert;
12use function func_get_args;
13use function func_num_args;
14use function is_object;
15use function sprintf;
16
17/** @internal */
18final class ReflectionReadonlyProperty extends ReflectionProperty
19{
20 public function __construct(
21 private readonly ReflectionProperty $wrappedProperty,
22 ) {
23 if (! $wrappedProperty->isReadOnly()) {
24 throw new InvalidArgumentException('Given property is not readonly.');
25 }
26
27 parent::__construct($wrappedProperty->class, $wrappedProperty->name);
28 }
29
30 public function getValue(object|null $object = null): mixed
31 {
32 return $this->wrappedProperty->getValue(...func_get_args());
33 }
34
35 public function setValue(mixed $objectOrValue, mixed $value = null): void
36 {
37 if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) {
38 $this->wrappedProperty->setValue(...func_get_args());
39
40 return;
41 }
42
43 assert(is_object($objectOrValue));
44
45 if (parent::getValue($objectOrValue) !== $value) {
46 throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
47 }
48 }
49}
diff --git a/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php b/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php
new file mode 100644
index 0000000..6c06e84
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class SequenceGenerator implements MappingAttribute
11{
12 public function __construct(
13 public readonly string|null $sequenceName = null,
14 public readonly int $allocationSize = 1,
15 public readonly int $initialValue = 1,
16 ) {
17 }
18}
diff --git a/vendor/doctrine/orm/src/Mapping/Table.php b/vendor/doctrine/orm/src/Mapping/Table.php
new file mode 100644
index 0000000..ac1e8ed
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Table.php
@@ -0,0 +1,45 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8use Doctrine\Deprecations\Deprecation;
9
10#[Attribute(Attribute::TARGET_CLASS)]
11final class Table implements MappingAttribute
12{
13 /**
14 * @param array<Index>|null $indexes
15 * @param array<UniqueConstraint>|null $uniqueConstraints
16 * @param array<string,mixed> $options
17 */
18 public function __construct(
19 public readonly string|null $name = null,
20 public readonly string|null $schema = null,
21 public readonly array|null $indexes = null,
22 public readonly array|null $uniqueConstraints = null,
23 public readonly array $options = [],
24 ) {
25 if ($this->indexes !== null) {
26 Deprecation::trigger(
27 'doctrine/orm',
28 'https://github.com/doctrine/orm/pull/11357',
29 'Providing the property $indexes on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
30 self::class,
31 Index::class,
32 );
33 }
34
35 if ($this->uniqueConstraints !== null) {
36 Deprecation::trigger(
37 'doctrine/orm',
38 'https://github.com/doctrine/orm/pull/11357',
39 'Providing the property $uniqueConstraints on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
40 self::class,
41 UniqueConstraint::class,
42 );
43 }
44 }
45}
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php
new file mode 100644
index 0000000..2e4969c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7interface ToManyAssociationMapping
8{
9 /** @psalm-assert-if-true string $this->indexBy() */
10 public function isIndexed(): bool;
11
12 public function indexBy(): string;
13
14 /** @return array<string, 'asc'|'desc'> */
15 public function orderBy(): array;
16}
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php
new file mode 100644
index 0000000..306880d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use LogicException;
8
9use function sprintf;
10
11/** @internal */
12trait ToManyAssociationMappingImplementation
13{
14 /**
15 * Specification of a field on target-entity that is used to index the
16 * collection by. This field HAS to be either the primary key or a unique
17 * column. Otherwise the collection does not contain all the entities that
18 * are actually related.
19 */
20 public string|null $indexBy = null;
21
22 /**
23 * A map of field names (of the target entity) to sorting directions
24 *
25 * @var array<string, 'asc'|'desc'>
26 */
27 public array $orderBy = [];
28
29 /** @return array<string, 'asc'|'desc'> */
30 final public function orderBy(): array
31 {
32 return $this->orderBy;
33 }
34
35 /** @psalm-assert-if-true !null $this->indexBy */
36 final public function isIndexed(): bool
37 {
38 return $this->indexBy !== null;
39 }
40
41 final public function indexBy(): string
42 {
43 if (! $this->isIndexed()) {
44 throw new LogicException(sprintf(
45 'This mapping is not indexed. Use %s::isIndexed() to check that before calling %s.',
46 self::class,
47 __METHOD__,
48 ));
49 }
50
51 return $this->indexBy;
52 }
53
54 /** @return list<string> */
55 public function __sleep(): array
56 {
57 $serialized = parent::__sleep();
58
59 if ($this->indexBy !== null) {
60 $serialized[] = 'indexBy';
61 }
62
63 if ($this->orderBy !== []) {
64 $serialized[] = 'orderBy';
65 }
66
67 return $serialized;
68 }
69}
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php
new file mode 100644
index 0000000..a092ebe
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7abstract class ToManyInverseSideMapping extends InverseSideMapping implements ToManyAssociationMapping
8{
9 use ToManyAssociationMappingImplementation;
10}
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php
new file mode 100644
index 0000000..92eca7c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7abstract class ToManyOwningSideMapping extends OwningSideMapping
8{
9 use ToManyAssociationMappingImplementation;
10}
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php
new file mode 100644
index 0000000..048055c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7interface ToOneAssociationMapping
8{
9}
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php
new file mode 100644
index 0000000..5be89e6
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php
@@ -0,0 +1,52 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7abstract class ToOneInverseSideMapping extends InverseSideMapping
8{
9 /**
10 * @param mixed[] $mappingArray
11 * @param class-string $name
12 * @psalm-param array{
13 * fieldName: string,
14 * sourceEntity: class-string,
15 * targetEntity: class-string,
16 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
17 * fetch?: ClassMetadata::FETCH_*|null,
18 * inherited?: class-string|null,
19 * declared?: class-string|null,
20 * cache?: array<mixed>|null,
21 * id?: bool|null,
22 * isOnDeleteCascade?: bool|null,
23 * originalClass?: class-string|null,
24 * originalField?: string|null,
25 * orphanRemoval?: bool,
26 * unique?: bool|null,
27 * joinTable?: mixed[]|null,
28 * type?: int,
29 * isOwningSide: bool,
30 * } $mappingArray
31 */
32 public static function fromMappingArrayAndName(
33 array $mappingArray,
34 string $name,
35 ): static {
36 $mapping = static::fromMappingArray($mappingArray);
37
38 if (isset($mapping->id) && $mapping->id === true) {
39 throw MappingException::illegalInverseIdentifierAssociation($name, $mapping->fieldName);
40 }
41
42 if ($mapping->orphanRemoval) {
43 if (! $mapping->isCascadeRemove()) {
44 $mapping->cascade[] = 'remove';
45 }
46
47 $mapping->unique = null;
48 }
49
50 return $mapping;
51 }
52}
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php
new file mode 100644
index 0000000..cb85afb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php
@@ -0,0 +1,212 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use RuntimeException;
8
9use function array_flip;
10use function assert;
11use function count;
12use function trim;
13
14abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOneAssociationMapping
15{
16 /** @var array<string, string> */
17 public array $sourceToTargetKeyColumns = [];
18
19 /** @var array<string, string> */
20 public array $targetToSourceKeyColumns = [];
21
22 /** @var list<JoinColumnMapping> */
23 public array $joinColumns = [];
24
25 /** @var array<string, string> */
26 public array $joinColumnFieldNames = [];
27
28 /**
29 * @param array<string, mixed> $mappingArray
30 * @psalm-param array{
31 * fieldName: string,
32 * sourceEntity: class-string,
33 * targetEntity: class-string,
34 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
35 * fetch?: ClassMetadata::FETCH_*|null,
36 * inherited?: class-string|null,
37 * declared?: class-string|null,
38 * cache?: array<mixed>|null,
39 * id?: bool|null,
40 * isOnDeleteCascade?: bool|null,
41 * originalClass?: class-string|null,
42 * originalField?: string|null,
43 * orphanRemoval?: bool,
44 * unique?: bool|null,
45 * joinTable?: mixed[]|null,
46 * type?: int,
47 * isOwningSide: bool,
48 * joinColumns?: mixed[]|null,
49 * } $mappingArray
50 */
51 public static function fromMappingArray(array $mappingArray): static
52 {
53 $joinColumns = $mappingArray['joinColumns'] ?? [];
54 unset($mappingArray['joinColumns']);
55
56 $instance = parent::fromMappingArray($mappingArray);
57 assert($instance->isToOneOwningSide());
58
59 foreach ($joinColumns as $column) {
60 $instance->joinColumns[] = JoinColumnMapping::fromMappingArray($column);
61 }
62
63 if ($instance->orphanRemoval) {
64 if (! $instance->isCascadeRemove()) {
65 $instance->cascade[] = 'remove';
66 }
67
68 $instance->unique = null;
69 }
70
71 return $instance;
72 }
73
74 /**
75 * @param mixed[] $mappingArray
76 * @param class-string $name
77 * @psalm-param array{
78 * fieldName: string,
79 * sourceEntity: class-string,
80 * targetEntity: class-string,
81 * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
82 * fetch?: ClassMetadata::FETCH_*|null,
83 * inherited?: class-string|null,
84 * declared?: class-string|null,
85 * cache?: array<mixed>|null,
86 * id?: bool|null,
87 * isOnDeleteCascade?: bool|null,
88 * originalClass?: class-string|null,
89 * originalField?: string|null,
90 * orphanRemoval?: bool,
91 * unique?: bool|null,
92 * joinTable?: mixed[]|null,
93 * type?: int,
94 * isOwningSide: bool,
95 * joinColumns?: mixed[]|null,
96 * } $mappingArray
97 */
98 public static function fromMappingArrayAndName(
99 array $mappingArray,
100 NamingStrategy $namingStrategy,
101 string $name,
102 array|null $table,
103 bool $isInheritanceTypeSingleTable,
104 ): static {
105 if (isset($mappingArray['joinColumns'])) {
106 foreach ($mappingArray['joinColumns'] as $index => $joinColumn) {
107 if (empty($joinColumn['name'])) {
108 $mappingArray['joinColumns'][$index]['name'] = $namingStrategy->joinColumnName($mappingArray['fieldName'], $name);
109 }
110 }
111 }
112
113 $mapping = static::fromMappingArray($mappingArray);
114
115 assert($mapping->isToOneOwningSide());
116 if (empty($mapping->joinColumns)) {
117 // Apply default join column
118 $mapping->joinColumns = [
119 JoinColumnMapping::fromMappingArray([
120 'name' => $namingStrategy->joinColumnName($mapping->fieldName, $name),
121 'referencedColumnName' => $namingStrategy->referenceColumnName(),
122 ]),
123 ];
124 }
125
126 $uniqueConstraintColumns = [];
127
128 foreach ($mapping->joinColumns as $joinColumn) {
129 if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) {
130 if (count($mapping->joinColumns) === 1) {
131 if (empty($mapping->id)) {
132 $joinColumn->unique = true;
133 }
134 } else {
135 $uniqueConstraintColumns[] = $joinColumn->name;
136 }
137 }
138
139 if (empty($joinColumn->referencedColumnName)) {
140 $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
141 }
142
143 if ($joinColumn->name[0] === '`') {
144 $joinColumn->name = trim($joinColumn->name, '`');
145 $joinColumn->quoted = true;
146 }
147
148 if ($joinColumn->referencedColumnName[0] === '`') {
149 $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`');
150 $joinColumn->quoted = true;
151 }
152
153 $mapping->sourceToTargetKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName;
154 $mapping->joinColumnFieldNames[$joinColumn->name] = $joinColumn->fieldName ?? $joinColumn->name;
155 }
156
157 if ($uniqueConstraintColumns) {
158 if (! $table) {
159 throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.');
160 }
161
162 $table['uniqueConstraints'][$mapping->fieldName . '_uniq'] = ['columns' => $uniqueConstraintColumns];
163 }
164
165 $mapping->targetToSourceKeyColumns = array_flip($mapping->sourceToTargetKeyColumns);
166
167 return $mapping;
168 }
169
170 public function offsetSet(mixed $offset, mixed $value): void
171 {
172 if ($offset === 'joinColumns') {
173 $joinColumns = [];
174 foreach ($value as $column) {
175 $joinColumns[] = JoinColumnMapping::fromMappingArray($column);
176 }
177
178 $this->joinColumns = $joinColumns;
179
180 return;
181 }
182
183 parent::offsetSet($offset, $value);
184 }
185
186 /** @return array<string, mixed> */
187 public function toArray(): array
188 {
189 $array = parent::toArray();
190
191 $joinColumns = [];
192 foreach ($array['joinColumns'] as $column) {
193 $joinColumns[] = (array) $column;
194 }
195
196 $array['joinColumns'] = $joinColumns;
197
198 return $array;
199 }
200
201 /** @return list<string> */
202 public function __sleep(): array
203 {
204 return [
205 ...parent::__sleep(),
206 'joinColumns',
207 'joinColumnFieldNames',
208 'sourceToTargetKeyColumns',
209 'targetToSourceKeyColumns',
210 ];
211 }
212}
diff --git a/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php
new file mode 100644
index 0000000..2db9e90
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use BackedEnum;
8use ReflectionProperty;
9
10interface TypedFieldMapper
11{
12 /**
13 * Validates & completes the given field mapping based on typed property.
14 *
15 * @param array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} $mapping The field mapping to validate & complete.
16 *
17 * @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
18 */
19 public function validateAndComplete(array $mapping, ReflectionProperty $field): array;
20}
diff --git a/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php b/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php
new file mode 100644
index 0000000..cedc150
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php
@@ -0,0 +1,108 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use function preg_replace;
8use function str_contains;
9use function strrpos;
10use function strtolower;
11use function strtoupper;
12use function substr;
13
14use const CASE_LOWER;
15use const CASE_UPPER;
16
17/**
18 * Naming strategy implementing the underscore naming convention.
19 * Converts 'MyEntity' to 'my_entity' or 'MY_ENTITY'.
20 *
21 * @link www.doctrine-project.org
22 */
23class UnderscoreNamingStrategy implements NamingStrategy
24{
25 /**
26 * Underscore naming strategy construct.
27 *
28 * @param int $case CASE_LOWER | CASE_UPPER
29 */
30 public function __construct(private int $case = CASE_LOWER)
31 {
32 }
33
34 /** @return int CASE_LOWER | CASE_UPPER */
35 public function getCase(): int
36 {
37 return $this->case;
38 }
39
40 /**
41 * Sets string case CASE_LOWER | CASE_UPPER.
42 * Alphabetic characters converted to lowercase or uppercase.
43 */
44 public function setCase(int $case): void
45 {
46 $this->case = $case;
47 }
48
49 public function classToTableName(string $className): string
50 {
51 if (str_contains($className, '\\')) {
52 $className = substr($className, strrpos($className, '\\') + 1);
53 }
54
55 return $this->underscore($className);
56 }
57
58 public function propertyToColumnName(string $propertyName, string $className): string
59 {
60 return $this->underscore($propertyName);
61 }
62
63 public function embeddedFieldToColumnName(
64 string $propertyName,
65 string $embeddedColumnName,
66 string $className,
67 string $embeddedClassName,
68 ): string {
69 return $this->underscore($propertyName) . '_' . $embeddedColumnName;
70 }
71
72 public function referenceColumnName(): string
73 {
74 return $this->case === CASE_UPPER ? 'ID' : 'id';
75 }
76
77 public function joinColumnName(string $propertyName, string $className): string
78 {
79 return $this->underscore($propertyName) . '_' . $this->referenceColumnName();
80 }
81
82 public function joinTableName(
83 string $sourceEntity,
84 string $targetEntity,
85 string $propertyName,
86 ): string {
87 return $this->classToTableName($sourceEntity) . '_' . $this->classToTableName($targetEntity);
88 }
89
90 public function joinKeyColumnName(
91 string $entityName,
92 string|null $referencedColumnName,
93 ): string {
94 return $this->classToTableName($entityName) . '_' .
95 ($referencedColumnName ?: $this->referenceColumnName());
96 }
97
98 private function underscore(string $string): string
99 {
100 $string = preg_replace('/(?<=[a-z0-9])([A-Z])/', '_$1', $string);
101
102 if ($this->case === CASE_UPPER) {
103 return strtoupper($string);
104 }
105
106 return strtolower($string);
107 }
108}
diff --git a/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php b/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php
new file mode 100644
index 0000000..3180be0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
10final class UniqueConstraint implements MappingAttribute
11{
12 /**
13 * @param array<string>|null $columns
14 * @param array<string>|null $fields
15 * @param array<string,mixed>|null $options
16 */
17 public function __construct(
18 public readonly string|null $name = null,
19 public readonly array|null $columns = null,
20 public readonly array|null $fields = null,
21 public readonly array|null $options = null,
22 ) {
23 }
24}
diff --git a/vendor/doctrine/orm/src/Mapping/Version.php b/vendor/doctrine/orm/src/Mapping/Version.php
new file mode 100644
index 0000000..7252e05
--- /dev/null
+++ b/vendor/doctrine/orm/src/Mapping/Version.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Mapping;
6
7use Attribute;
8
9#[Attribute(Attribute::TARGET_PROPERTY)]
10final class Version implements MappingAttribute
11{
12}