summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src')
-rw-r--r--vendor/doctrine/orm/src/AbstractQuery.php1116
-rw-r--r--vendor/doctrine/orm/src/Cache.php106
-rw-r--r--vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php30
-rw-r--r--vendor/doctrine/orm/src/Cache/CacheConfiguration.php60
-rw-r--r--vendor/doctrine/orm/src/Cache/CacheEntry.php16
-rw-r--r--vendor/doctrine/orm/src/Cache/CacheException.php26
-rw-r--r--vendor/doctrine/orm/src/Cache/CacheFactory.php64
-rw-r--r--vendor/doctrine/orm/src/Cache/CacheKey.php16
-rw-r--r--vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php25
-rw-r--r--vendor/doctrine/orm/src/Cache/CollectionCacheKey.php39
-rw-r--r--vendor/doctrine/orm/src/Cache/CollectionHydrator.php21
-rw-r--r--vendor/doctrine/orm/src/Cache/ConcurrentRegion.php36
-rw-r--r--vendor/doctrine/orm/src/Cache/DefaultCache.php245
-rw-r--r--vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php189
-rw-r--r--vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php75
-rw-r--r--vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php176
-rw-r--r--vendor/doctrine/orm/src/Cache/DefaultQueryCache.php414
-rw-r--r--vendor/doctrine/orm/src/Cache/EntityCacheEntry.php50
-rw-r--r--vendor/doctrine/orm/src/Cache/EntityCacheKey.php38
-rw-r--r--vendor/doctrine/orm/src/Cache/EntityHydrator.php28
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/CacheException.php14
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php19
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php15
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php23
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php18
-rw-r--r--vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php19
-rw-r--r--vendor/doctrine/orm/src/Cache/Lock.php25
-rw-r--r--vendor/doctrine/orm/src/Cache/LockException.php14
-rw-r--r--vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php60
-rw-r--r--vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php94
-rw-r--r--vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php174
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php25
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php168
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php36
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php74
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php24
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php103
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php557
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php20
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php85
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php19
-rw-r--r--vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php105
-rw-r--r--vendor/doctrine/orm/src/Cache/QueryCache.php28
-rw-r--r--vendor/doctrine/orm/src/Cache/QueryCacheEntry.php29
-rw-r--r--vendor/doctrine/orm/src/Cache/QueryCacheKey.php23
-rw-r--r--vendor/doctrine/orm/src/Cache/QueryCacheValidator.php16
-rw-r--r--vendor/doctrine/orm/src/Cache/Region.php73
-rw-r--r--vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php113
-rw-r--r--vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php194
-rw-r--r--vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php20
-rw-r--r--vendor/doctrine/orm/src/Cache/RegionsConfiguration.php63
-rw-r--r--vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php29
-rw-r--r--vendor/doctrine/orm/src/Cache/TimestampCacheKey.php17
-rw-r--r--vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php38
-rw-r--r--vendor/doctrine/orm/src/Cache/TimestampRegion.php18
-rw-r--r--vendor/doctrine/orm/src/Configuration.php649
-rw-r--r--vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php174
-rw-r--r--vendor/doctrine/orm/src/EntityManager.php626
-rw-r--r--vendor/doctrine/orm/src/EntityManagerInterface.php242
-rw-r--r--vendor/doctrine/orm/src/EntityNotFoundException.php46
-rw-r--r--vendor/doctrine/orm/src/EntityRepository.php236
-rw-r--r--vendor/doctrine/orm/src/Event/ListenersInvoker.php98
-rw-r--r--vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php25
-rw-r--r--vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php49
-rw-r--r--vendor/doctrine/orm/src/Event/OnClearEventArgs.php19
-rw-r--r--vendor/doctrine/orm/src/Event/OnFlushEventArgs.php19
-rw-r--r--vendor/doctrine/orm/src/Event/PostFlushEventArgs.php19
-rw-r--r--vendor/doctrine/orm/src/Event/PostLoadEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PostPersistEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PreFlushEventArgs.php19
-rw-r--r--vendor/doctrine/orm/src/Event/PrePersistEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php13
-rw-r--r--vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php100
-rw-r--r--vendor/doctrine/orm/src/Events.php125
-rw-r--r--vendor/doctrine/orm/src/Exception/ConfigurationException.php9
-rw-r--r--vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php39
-rw-r--r--vendor/doctrine/orm/src/Exception/EntityManagerClosed.php15
-rw-r--r--vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php20
-rw-r--r--vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php18
-rw-r--r--vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php17
-rw-r--r--vendor/doctrine/orm/src/Exception/ManagerException.php11
-rw-r--r--vendor/doctrine/orm/src/Exception/MissingIdentifierField.php21
-rw-r--r--vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php18
-rw-r--r--vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php26
-rw-r--r--vendor/doctrine/orm/src/Exception/NotSupported.php44
-rw-r--r--vendor/doctrine/orm/src/Exception/ORMException.php11
-rw-r--r--vendor/doctrine/orm/src/Exception/PersisterException.php11
-rw-r--r--vendor/doctrine/orm/src/Exception/RepositoryException.php13
-rw-r--r--vendor/doctrine/orm/src/Exception/SchemaToolException.php11
-rw-r--r--vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php27
-rw-r--r--vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php23
-rw-r--r--vendor/doctrine/orm/src/Id/AbstractIdGenerator.php28
-rw-r--r--vendor/doctrine/orm/src/Id/AssignedGenerator.php45
-rw-r--r--vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php25
-rw-r--r--vendor/doctrine/orm/src/Id/IdentityGenerator.php25
-rw-r--r--vendor/doctrine/orm/src/Id/SequenceGenerator.php112
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php556
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php270
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php67
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php586
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php34
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php35
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php176
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php40
-rw-r--r--vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php64
-rw-r--r--vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php55
-rw-r--r--vendor/doctrine/orm/src/Internal/QueryType.php13
-rw-r--r--vendor/doctrine/orm/src/Internal/SQLResultCasing.php30
-rw-r--r--vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php159
-rw-r--r--vendor/doctrine/orm/src/Internal/TopologicalSort.php155
-rw-r--r--vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php47
-rw-r--r--vendor/doctrine/orm/src/LazyCriteriaCollection.php96
-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
-rw-r--r--vendor/doctrine/orm/src/NativeQuery.php68
-rw-r--r--vendor/doctrine/orm/src/NoResultException.php16
-rw-r--r--vendor/doctrine/orm/src/NonUniqueResultException.php18
-rw-r--r--vendor/doctrine/orm/src/ORMInvalidArgumentException.php195
-rw-r--r--vendor/doctrine/orm/src/ORMSetup.php127
-rw-r--r--vendor/doctrine/orm/src/OptimisticLockException.php55
-rw-r--r--vendor/doctrine/orm/src/PersistentCollection.php652
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php50
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php59
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php770
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php264
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php66
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php2085
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php60
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php298
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php601
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php166
-rw-r--r--vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php15
-rw-r--r--vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php15
-rw-r--r--vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php24
-rw-r--r--vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php22
-rw-r--r--vendor/doctrine/orm/src/Persisters/PersisterException.php23
-rw-r--r--vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php79
-rw-r--r--vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php88
-rw-r--r--vendor/doctrine/orm/src/PessimisticLockException.php16
-rw-r--r--vendor/doctrine/orm/src/Proxy/Autoloader.php86
-rw-r--r--vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php35
-rw-r--r--vendor/doctrine/orm/src/Proxy/InternalProxy.php18
-rw-r--r--vendor/doctrine/orm/src/Proxy/NotAProxyClass.php22
-rw-r--r--vendor/doctrine/orm/src/Proxy/ProxyFactory.php439
-rw-r--r--vendor/doctrine/orm/src/Query.php682
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ASTException.php20
-rw-r--r--vendor/doctrine/orm/src/Query/AST/AggregateExpression.php23
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php34
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php36
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/BetweenExpression.php23
-rw-r--r--vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php32
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php34
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/DeleteClause.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/DeleteStatement.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ExistsExpression.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/FromClause.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php37
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php43
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php43
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php58
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php35
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php83
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php41
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php62
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php32
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php90
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php45
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php62
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php40
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php43
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php113
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php40
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php58
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php119
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php40
-rw-r--r--vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/GroupByClause.php20
-rw-r--r--vendor/doctrine/orm/src/Query/AST/HavingClause.php19
-rw-r--r--vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php28
-rw-r--r--vendor/doctrine/orm/src/Query/AST/InListExpression.php23
-rw-r--r--vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php22
-rw-r--r--vendor/doctrine/orm/src/Query/AST/IndexBy.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/InputParameter.php35
-rw-r--r--vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Join.php34
-rw-r--r--vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php19
-rw-r--r--vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php24
-rw-r--r--vendor/doctrine/orm/src/Query/AST/LikeExpression.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Literal.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Node.php85
-rw-r--r--vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/NullIfExpression.php24
-rw-r--r--vendor/doctrine/orm/src/Query/AST/OrderByClause.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/OrderByItem.php38
-rw-r--r--vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php22
-rw-r--r--vendor/doctrine/orm/src/Query/AST/PathExpression.php39
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php17
-rw-r--r--vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php43
-rw-r--r--vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SelectClause.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SelectExpression.php28
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SelectStatement.php32
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php28
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php27
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/Subselect.php32
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php25
-rw-r--r--vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php19
-rw-r--r--vendor/doctrine/orm/src/Query/AST/TypedExpression.php15
-rw-r--r--vendor/doctrine/orm/src/Query/AST/UpdateClause.php29
-rw-r--r--vendor/doctrine/orm/src/Query/AST/UpdateItem.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/UpdateStatement.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/WhenClause.php26
-rw-r--r--vendor/doctrine/orm/src/Query/AST/WhereClause.php24
-rw-r--r--vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php61
-rw-r--r--vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php131
-rw-r--r--vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php180
-rw-r--r--vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php31
-rw-r--r--vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php42
-rw-r--r--vendor/doctrine/orm/src/Query/Expr.php615
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Andx.php32
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Base.php96
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Comparison.php47
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Composite.php50
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/From.php48
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Func.php48
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/GroupBy.php25
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Join.php77
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Literal.php25
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Math.php59
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/OrderBy.php60
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Orx.php32
-rw-r--r--vendor/doctrine/orm/src/Query/Expr/Select.php28
-rw-r--r--vendor/doctrine/orm/src/Query/Filter/FilterException.php23
-rw-r--r--vendor/doctrine/orm/src/Query/Filter/SQLFilter.php174
-rw-r--r--vendor/doctrine/orm/src/Query/FilterCollection.php260
-rw-r--r--vendor/doctrine/orm/src/Query/Lexer.php150
-rw-r--r--vendor/doctrine/orm/src/Query/Parameter.php89
-rw-r--r--vendor/doctrine/orm/src/Query/ParameterTypeInferer.php77
-rw-r--r--vendor/doctrine/orm/src/Query/Parser.php3269
-rw-r--r--vendor/doctrine/orm/src/Query/ParserResult.php118
-rw-r--r--vendor/doctrine/orm/src/Query/Printer.php64
-rw-r--r--vendor/doctrine/orm/src/Query/QueryException.php155
-rw-r--r--vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php180
-rw-r--r--vendor/doctrine/orm/src/Query/ResultSetMapping.php547
-rw-r--r--vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php281
-rw-r--r--vendor/doctrine/orm/src/Query/SqlWalker.php2264
-rw-r--r--vendor/doctrine/orm/src/Query/TokenType.php91
-rw-r--r--vendor/doctrine/orm/src/Query/TreeWalker.php44
-rw-r--r--vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php90
-rw-r--r--vendor/doctrine/orm/src/Query/TreeWalkerChain.php88
-rw-r--r--vendor/doctrine/orm/src/QueryBuilder.php1375
-rw-r--r--vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php49
-rw-r--r--vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php21
-rw-r--r--vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php27
-rw-r--r--vendor/doctrine/orm/src/Repository/RepositoryFactory.php26
-rw-r--r--vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php69
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php25
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php119
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php110
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php52
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php54
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php101
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php65
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php96
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php80
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php279
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php118
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php39
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php75
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php116
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php147
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php89
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php88
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php14
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php26
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php31
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php92
-rw-r--r--vendor/doctrine/orm/src/Tools/Debug.php158
-rw-r--r--vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php144
-rw-r--r--vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php33
-rw-r--r--vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php40
-rw-r--r--vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/Exception/NotSupported.php16
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php125
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php68
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php16
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php544
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php155
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/Paginator.php263
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php48
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php40
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php116
-rw-r--r--vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php117
-rw-r--r--vendor/doctrine/orm/src/Tools/SchemaTool.php932
-rw-r--r--vendor/doctrine/orm/src/Tools/SchemaValidator.php443
-rw-r--r--vendor/doctrine/orm/src/Tools/ToolEvents.php23
-rw-r--r--vendor/doctrine/orm/src/Tools/ToolsException.php24
-rw-r--r--vendor/doctrine/orm/src/TransactionRequiredException.php21
-rw-r--r--vendor/doctrine/orm/src/UnexpectedResultException.php15
-rw-r--r--vendor/doctrine/orm/src/UnitOfWork.php3252
-rw-r--r--vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php44
-rw-r--r--vendor/doctrine/orm/src/Utility/IdentifierFlattener.php83
-rw-r--r--vendor/doctrine/orm/src/Utility/LockSqlHelper.php35
-rw-r--r--vendor/doctrine/orm/src/Utility/PersisterHelper.php108
425 files changed, 51805 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/AbstractQuery.php b/vendor/doctrine/orm/src/AbstractQuery.php
new file mode 100644
index 0000000..0ff92c3
--- /dev/null
+++ b/vendor/doctrine/orm/src/AbstractQuery.php
@@ -0,0 +1,1116 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use BackedEnum;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\DBAL\ArrayParameterType;
11use Doctrine\DBAL\Cache\QueryCacheProfile;
12use Doctrine\DBAL\ParameterType;
13use Doctrine\DBAL\Result;
14use Doctrine\ORM\Cache\Logging\CacheLogger;
15use Doctrine\ORM\Cache\QueryCacheKey;
16use Doctrine\ORM\Cache\TimestampCacheKey;
17use Doctrine\ORM\Mapping\ClassMetadata;
18use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
19use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
20use Doctrine\ORM\Query\Parameter;
21use Doctrine\ORM\Query\QueryException;
22use Doctrine\ORM\Query\ResultSetMapping;
23use Doctrine\Persistence\Mapping\MappingException;
24use LogicException;
25use Psr\Cache\CacheItemPoolInterface;
26use Traversable;
27
28use function array_map;
29use function array_shift;
30use function assert;
31use function count;
32use function is_array;
33use function is_numeric;
34use function is_object;
35use function is_scalar;
36use function is_string;
37use function iterator_to_array;
38use function ksort;
39use function reset;
40use function serialize;
41use function sha1;
42
43/**
44 * Base contract for ORM queries. Base class for Query and NativeQuery.
45 *
46 * @link www.doctrine-project.org
47 */
48abstract class AbstractQuery
49{
50 /* Hydration mode constants */
51
52 /**
53 * Hydrates an object graph. This is the default behavior.
54 */
55 public const HYDRATE_OBJECT = 1;
56
57 /**
58 * Hydrates an array graph.
59 */
60 public const HYDRATE_ARRAY = 2;
61
62 /**
63 * Hydrates a flat, rectangular result set with scalar values.
64 */
65 public const HYDRATE_SCALAR = 3;
66
67 /**
68 * Hydrates a single scalar value.
69 */
70 public const HYDRATE_SINGLE_SCALAR = 4;
71
72 /**
73 * Very simple object hydrator (optimized for performance).
74 */
75 public const HYDRATE_SIMPLEOBJECT = 5;
76
77 /**
78 * Hydrates scalar column value.
79 */
80 public const HYDRATE_SCALAR_COLUMN = 6;
81
82 /**
83 * The parameter map of this query.
84 *
85 * @var ArrayCollection|Parameter[]
86 * @psalm-var ArrayCollection<int, Parameter>
87 */
88 protected ArrayCollection $parameters;
89
90 /**
91 * The user-specified ResultSetMapping to use.
92 */
93 protected ResultSetMapping|null $resultSetMapping = null;
94
95 /**
96 * The map of query hints.
97 *
98 * @psalm-var array<string, mixed>
99 */
100 protected array $hints = [];
101
102 /**
103 * The hydration mode.
104 *
105 * @psalm-var string|AbstractQuery::HYDRATE_*
106 */
107 protected string|int $hydrationMode = self::HYDRATE_OBJECT;
108
109 protected QueryCacheProfile|null $queryCacheProfile = null;
110
111 /**
112 * Whether or not expire the result cache.
113 */
114 protected bool $expireResultCache = false;
115
116 protected QueryCacheProfile|null $hydrationCacheProfile = null;
117
118 /**
119 * Whether to use second level cache, if available.
120 */
121 protected bool $cacheable = false;
122
123 protected bool $hasCache = false;
124
125 /**
126 * Second level cache region name.
127 */
128 protected string|null $cacheRegion = null;
129
130 /**
131 * Second level query cache mode.
132 *
133 * @psalm-var Cache::MODE_*|null
134 */
135 protected int|null $cacheMode = null;
136
137 protected CacheLogger|null $cacheLogger = null;
138
139 protected int $lifetime = 0;
140
141 /**
142 * Initializes a new instance of a class derived from <tt>AbstractQuery</tt>.
143 */
144 public function __construct(
145 /**
146 * The entity manager used by this query object.
147 */
148 protected EntityManagerInterface $em,
149 ) {
150 $this->parameters = new ArrayCollection();
151 $this->hints = $em->getConfiguration()->getDefaultQueryHints();
152 $this->hasCache = $this->em->getConfiguration()->isSecondLevelCacheEnabled();
153
154 if ($this->hasCache) {
155 $this->cacheLogger = $em->getConfiguration()
156 ->getSecondLevelCacheConfiguration()
157 ->getCacheLogger();
158 }
159 }
160
161 /**
162 * Enable/disable second level query (result) caching for this query.
163 *
164 * @return $this
165 */
166 public function setCacheable(bool $cacheable): static
167 {
168 $this->cacheable = $cacheable;
169
170 return $this;
171 }
172
173 /** @return bool TRUE if the query results are enabled for second level cache, FALSE otherwise. */
174 public function isCacheable(): bool
175 {
176 return $this->cacheable;
177 }
178
179 /** @return $this */
180 public function setCacheRegion(string $cacheRegion): static
181 {
182 $this->cacheRegion = $cacheRegion;
183
184 return $this;
185 }
186
187 /**
188 * Obtain the name of the second level query cache region in which query results will be stored
189 *
190 * @return string|null The cache region name; NULL indicates the default region.
191 */
192 public function getCacheRegion(): string|null
193 {
194 return $this->cacheRegion;
195 }
196
197 /** @return bool TRUE if the query cache and second level cache are enabled, FALSE otherwise. */
198 protected function isCacheEnabled(): bool
199 {
200 return $this->cacheable && $this->hasCache;
201 }
202
203 public function getLifetime(): int
204 {
205 return $this->lifetime;
206 }
207
208 /**
209 * Sets the life-time for this query into second level cache.
210 *
211 * @return $this
212 */
213 public function setLifetime(int $lifetime): static
214 {
215 $this->lifetime = $lifetime;
216
217 return $this;
218 }
219
220 /** @psalm-return Cache::MODE_*|null */
221 public function getCacheMode(): int|null
222 {
223 return $this->cacheMode;
224 }
225
226 /**
227 * @psalm-param Cache::MODE_* $cacheMode
228 *
229 * @return $this
230 */
231 public function setCacheMode(int $cacheMode): static
232 {
233 $this->cacheMode = $cacheMode;
234
235 return $this;
236 }
237
238 /**
239 * Gets the SQL query that corresponds to this query object.
240 * The returned SQL syntax depends on the connection driver that is used
241 * by this query object at the time of this method call.
242 *
243 * @return list<string>|string SQL query
244 */
245 abstract public function getSQL(): string|array;
246
247 /**
248 * Retrieves the associated EntityManager of this Query instance.
249 */
250 public function getEntityManager(): EntityManagerInterface
251 {
252 return $this->em;
253 }
254
255 /**
256 * Frees the resources used by the query object.
257 *
258 * Resets Parameters, Parameter Types and Query Hints.
259 */
260 public function free(): void
261 {
262 $this->parameters = new ArrayCollection();
263
264 $this->hints = $this->em->getConfiguration()->getDefaultQueryHints();
265 }
266
267 /**
268 * Get all defined parameters.
269 *
270 * @psalm-return ArrayCollection<int, Parameter>
271 */
272 public function getParameters(): ArrayCollection
273 {
274 return $this->parameters;
275 }
276
277 /**
278 * Gets a query parameter.
279 *
280 * @param int|string $key The key (index or name) of the bound parameter.
281 *
282 * @return Parameter|null The value of the bound parameter, or NULL if not available.
283 */
284 public function getParameter(int|string $key): Parameter|null
285 {
286 $key = Parameter::normalizeName($key);
287
288 $filteredParameters = $this->parameters->filter(
289 static fn (Parameter $parameter): bool => $parameter->getName() === $key
290 );
291
292 return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
293 }
294
295 /**
296 * Sets a collection of query parameters.
297 *
298 * @param ArrayCollection|mixed[] $parameters
299 * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
300 *
301 * @return $this
302 */
303 public function setParameters(ArrayCollection|array $parameters): static
304 {
305 if (is_array($parameters)) {
306 /** @psalm-var ArrayCollection<int, Parameter> $parameterCollection */
307 $parameterCollection = new ArrayCollection();
308
309 foreach ($parameters as $key => $value) {
310 $parameterCollection->add(new Parameter($key, $value));
311 }
312
313 $parameters = $parameterCollection;
314 }
315
316 $this->parameters = $parameters;
317
318 return $this;
319 }
320
321 /**
322 * Sets a query parameter.
323 *
324 * @param string|int $key The parameter position or name.
325 * @param mixed $value The parameter value.
326 * @param ParameterType|ArrayParameterType|string|int|null $type The parameter type. If specified, the given value
327 * will be run through the type conversion of this
328 * type. This is usually not needed for strings and
329 * numeric types.
330 *
331 * @return $this
332 */
333 public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
334 {
335 $existingParameter = $this->getParameter($key);
336
337 if ($existingParameter !== null) {
338 $existingParameter->setValue($value, $type);
339
340 return $this;
341 }
342
343 $this->parameters->add(new Parameter($key, $value, $type));
344
345 return $this;
346 }
347
348 /**
349 * Processes an individual parameter value.
350 *
351 * @throws ORMInvalidArgumentException
352 */
353 public function processParameterValue(mixed $value): mixed
354 {
355 if (is_scalar($value)) {
356 return $value;
357 }
358
359 if ($value instanceof Collection) {
360 $value = iterator_to_array($value);
361 }
362
363 if (is_array($value)) {
364 $value = $this->processArrayParameterValue($value);
365
366 return $value;
367 }
368
369 if ($value instanceof ClassMetadata) {
370 return $value->name;
371 }
372
373 if ($value instanceof BackedEnum) {
374 return $value->value;
375 }
376
377 if (! is_object($value)) {
378 return $value;
379 }
380
381 try {
382 $class = DefaultProxyClassNameResolver::getClass($value);
383 $value = $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
384
385 if ($value === null) {
386 throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
387 }
388 } catch (MappingException | ORMMappingException) {
389 /* Silence any mapping exceptions. These can occur if the object in
390 question is not a mapped entity, in which case we just don't do
391 any preparation on the value.
392 Depending on MappingDriver, either MappingException or
393 ORMMappingException is thrown. */
394
395 $value = $this->potentiallyProcessIterable($value);
396 }
397
398 return $value;
399 }
400
401 /**
402 * If no mapping is detected, trying to resolve the value as a Traversable
403 */
404 private function potentiallyProcessIterable(mixed $value): mixed
405 {
406 if ($value instanceof Traversable) {
407 $value = iterator_to_array($value);
408 $value = $this->processArrayParameterValue($value);
409 }
410
411 return $value;
412 }
413
414 /**
415 * Process a parameter value which was previously identified as an array
416 *
417 * @param mixed[] $value
418 *
419 * @return mixed[]
420 */
421 private function processArrayParameterValue(array $value): array
422 {
423 foreach ($value as $key => $paramValue) {
424 $paramValue = $this->processParameterValue($paramValue);
425 $value[$key] = is_array($paramValue) ? reset($paramValue) : $paramValue;
426 }
427
428 return $value;
429 }
430
431 /**
432 * Sets the ResultSetMapping that should be used for hydration.
433 *
434 * @return $this
435 */
436 public function setResultSetMapping(ResultSetMapping $rsm): static
437 {
438 $this->translateNamespaces($rsm);
439 $this->resultSetMapping = $rsm;
440
441 return $this;
442 }
443
444 /**
445 * Gets the ResultSetMapping used for hydration.
446 */
447 protected function getResultSetMapping(): ResultSetMapping|null
448 {
449 return $this->resultSetMapping;
450 }
451
452 /**
453 * Allows to translate entity namespaces to full qualified names.
454 */
455 private function translateNamespaces(ResultSetMapping $rsm): void
456 {
457 $translate = fn ($alias): string => $this->em->getClassMetadata($alias)->getName();
458
459 $rsm->aliasMap = array_map($translate, $rsm->aliasMap);
460 $rsm->declaringClasses = array_map($translate, $rsm->declaringClasses);
461 }
462
463 /**
464 * Set a cache profile for hydration caching.
465 *
466 * If no result cache driver is set in the QueryCacheProfile, the default
467 * result cache driver is used from the configuration.
468 *
469 * Important: Hydration caching does NOT register entities in the
470 * UnitOfWork when retrieved from the cache. Never use result cached
471 * entities for requests that also flush the EntityManager. If you want
472 * some form of caching with UnitOfWork registration you should use
473 * {@see AbstractQuery::setResultCacheProfile()}.
474 *
475 * @return $this
476 *
477 * @example
478 * $lifetime = 100;
479 * $resultKey = "abc";
480 * $query->setHydrationCacheProfile(new QueryCacheProfile());
481 * $query->setHydrationCacheProfile(new QueryCacheProfile($lifetime, $resultKey));
482 */
483 public function setHydrationCacheProfile(QueryCacheProfile|null $profile): static
484 {
485 if ($profile === null) {
486 $this->hydrationCacheProfile = null;
487
488 return $this;
489 }
490
491 if (! $profile->getResultCache()) {
492 $defaultHydrationCacheImpl = $this->em->getConfiguration()->getHydrationCache();
493 if ($defaultHydrationCacheImpl) {
494 $profile = $profile->setResultCache($defaultHydrationCacheImpl);
495 }
496 }
497
498 $this->hydrationCacheProfile = $profile;
499
500 return $this;
501 }
502
503 public function getHydrationCacheProfile(): QueryCacheProfile|null
504 {
505 return $this->hydrationCacheProfile;
506 }
507
508 /**
509 * Set a cache profile for the result cache.
510 *
511 * If no result cache driver is set in the QueryCacheProfile, the default
512 * result cache driver is used from the configuration.
513 *
514 * @return $this
515 */
516 public function setResultCacheProfile(QueryCacheProfile|null $profile): static
517 {
518 if ($profile === null) {
519 $this->queryCacheProfile = null;
520
521 return $this;
522 }
523
524 if (! $profile->getResultCache()) {
525 $defaultResultCache = $this->em->getConfiguration()->getResultCache();
526 if ($defaultResultCache) {
527 $profile = $profile->setResultCache($defaultResultCache);
528 }
529 }
530
531 $this->queryCacheProfile = $profile;
532
533 return $this;
534 }
535
536 /**
537 * Defines a cache driver to be used for caching result sets and implicitly enables caching.
538 */
539 public function setResultCache(CacheItemPoolInterface|null $resultCache): static
540 {
541 if ($resultCache === null) {
542 if ($this->queryCacheProfile) {
543 $this->queryCacheProfile = new QueryCacheProfile($this->queryCacheProfile->getLifetime(), $this->queryCacheProfile->getCacheKey());
544 }
545
546 return $this;
547 }
548
549 $this->queryCacheProfile = $this->queryCacheProfile
550 ? $this->queryCacheProfile->setResultCache($resultCache)
551 : new QueryCacheProfile(0, null, $resultCache);
552
553 return $this;
554 }
555
556 /**
557 * Enables caching of the results of this query, for given or default amount of seconds
558 * and optionally specifies which ID to use for the cache entry.
559 *
560 * @param int|null $lifetime How long the cache entry is valid, in seconds.
561 * @param string|null $resultCacheId ID to use for the cache entry.
562 *
563 * @return $this
564 */
565 public function enableResultCache(int|null $lifetime = null, string|null $resultCacheId = null): static
566 {
567 $this->setResultCacheLifetime($lifetime);
568 $this->setResultCacheId($resultCacheId);
569
570 return $this;
571 }
572
573 /**
574 * Disables caching of the results of this query.
575 *
576 * @return $this
577 */
578 public function disableResultCache(): static
579 {
580 $this->queryCacheProfile = null;
581
582 return $this;
583 }
584
585 /**
586 * Defines how long the result cache will be active before expire.
587 *
588 * @param int|null $lifetime How long the cache entry is valid, in seconds.
589 *
590 * @return $this
591 */
592 public function setResultCacheLifetime(int|null $lifetime): static
593 {
594 $lifetime = (int) $lifetime;
595
596 if ($this->queryCacheProfile) {
597 $this->queryCacheProfile = $this->queryCacheProfile->setLifetime($lifetime);
598
599 return $this;
600 }
601
602 $this->queryCacheProfile = new QueryCacheProfile($lifetime);
603
604 $cache = $this->em->getConfiguration()->getResultCache();
605 if (! $cache) {
606 return $this;
607 }
608
609 $this->queryCacheProfile = $this->queryCacheProfile->setResultCache($cache);
610
611 return $this;
612 }
613
614 /**
615 * Defines if the result cache is active or not.
616 *
617 * @param bool $expire Whether or not to force resultset cache expiration.
618 *
619 * @return $this
620 */
621 public function expireResultCache(bool $expire = true): static
622 {
623 $this->expireResultCache = $expire;
624
625 return $this;
626 }
627
628 /**
629 * Retrieves if the resultset cache is active or not.
630 */
631 public function getExpireResultCache(): bool
632 {
633 return $this->expireResultCache;
634 }
635
636 public function getQueryCacheProfile(): QueryCacheProfile|null
637 {
638 return $this->queryCacheProfile;
639 }
640
641 /**
642 * Change the default fetch mode of an association for this query.
643 *
644 * @param class-string $class
645 * @psalm-param Mapping\ClassMetadata::FETCH_EAGER|Mapping\ClassMetadata::FETCH_LAZY $fetchMode
646 */
647 public function setFetchMode(string $class, string $assocName, int $fetchMode): static
648 {
649 $this->hints['fetchMode'][$class][$assocName] = $fetchMode;
650
651 return $this;
652 }
653
654 /**
655 * Defines the processing mode to be used during hydration / result set transformation.
656 *
657 * @param string|int $hydrationMode Doctrine processing mode to be used during hydration process.
658 * One of the Query::HYDRATE_* constants.
659 * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
660 *
661 * @return $this
662 */
663 public function setHydrationMode(string|int $hydrationMode): static
664 {
665 $this->hydrationMode = $hydrationMode;
666
667 return $this;
668 }
669
670 /**
671 * Gets the hydration mode currently used by the query.
672 *
673 * @psalm-return string|AbstractQuery::HYDRATE_*
674 */
675 public function getHydrationMode(): string|int
676 {
677 return $this->hydrationMode;
678 }
679
680 /**
681 * Gets the list of results for the query.
682 *
683 * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT).
684 *
685 * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
686 */
687 public function getResult(string|int $hydrationMode = self::HYDRATE_OBJECT): mixed
688 {
689 return $this->execute(null, $hydrationMode);
690 }
691
692 /**
693 * Gets the array of results for the query.
694 *
695 * Alias for execute(null, HYDRATE_ARRAY).
696 *
697 * @return mixed[]
698 */
699 public function getArrayResult(): array
700 {
701 return $this->execute(null, self::HYDRATE_ARRAY);
702 }
703
704 /**
705 * Gets one-dimensional array of results for the query.
706 *
707 * Alias for execute(null, HYDRATE_SCALAR_COLUMN).
708 *
709 * @return mixed[]
710 */
711 public function getSingleColumnResult(): array
712 {
713 return $this->execute(null, self::HYDRATE_SCALAR_COLUMN);
714 }
715
716 /**
717 * Gets the scalar results for the query.
718 *
719 * Alias for execute(null, HYDRATE_SCALAR).
720 *
721 * @return mixed[]
722 */
723 public function getScalarResult(): array
724 {
725 return $this->execute(null, self::HYDRATE_SCALAR);
726 }
727
728 /**
729 * Get exactly one result or null.
730 *
731 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
732 *
733 * @throws NonUniqueResultException
734 */
735 public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed
736 {
737 try {
738 $result = $this->execute(null, $hydrationMode);
739 } catch (NoResultException) {
740 return null;
741 }
742
743 if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
744 return null;
745 }
746
747 if (! is_array($result)) {
748 return $result;
749 }
750
751 if (count($result) > 1) {
752 throw new NonUniqueResultException();
753 }
754
755 return array_shift($result);
756 }
757
758 /**
759 * Gets the single result of the query.
760 *
761 * Enforces the presence as well as the uniqueness of the result.
762 *
763 * If the result is not unique, a NonUniqueResultException is thrown.
764 * If there is no result, a NoResultException is thrown.
765 *
766 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
767 *
768 * @throws NonUniqueResultException If the query result is not unique.
769 * @throws NoResultException If the query returned no result.
770 */
771 public function getSingleResult(string|int|null $hydrationMode = null): mixed
772 {
773 $result = $this->execute(null, $hydrationMode);
774
775 if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) {
776 throw new NoResultException();
777 }
778
779 if (! is_array($result)) {
780 return $result;
781 }
782
783 if (count($result) > 1) {
784 throw new NonUniqueResultException();
785 }
786
787 return array_shift($result);
788 }
789
790 /**
791 * Gets the single scalar result of the query.
792 *
793 * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR).
794 *
795 * @return bool|float|int|string|null The scalar result.
796 *
797 * @throws NoResultException If the query returned no result.
798 * @throws NonUniqueResultException If the query result is not unique.
799 */
800 public function getSingleScalarResult(): mixed
801 {
802 return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR);
803 }
804
805 /**
806 * Sets a query hint. If the hint name is not recognized, it is silently ignored.
807 *
808 * @return $this
809 */
810 public function setHint(string $name, mixed $value): static
811 {
812 $this->hints[$name] = $value;
813
814 return $this;
815 }
816
817 /**
818 * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned.
819 *
820 * @return mixed The value of the hint or FALSE, if the hint name is not recognized.
821 */
822 public function getHint(string $name): mixed
823 {
824 return $this->hints[$name] ?? false;
825 }
826
827 public function hasHint(string $name): bool
828 {
829 return isset($this->hints[$name]);
830 }
831
832 /**
833 * Return the key value map of query hints that are currently set.
834 *
835 * @return array<string,mixed>
836 */
837 public function getHints(): array
838 {
839 return $this->hints;
840 }
841
842 /**
843 * Executes the query and returns an iterable that can be used to incrementally
844 * iterate over the result.
845 *
846 * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters
847 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
848 *
849 * @return iterable<mixed>
850 */
851 public function toIterable(
852 ArrayCollection|array $parameters = [],
853 string|int|null $hydrationMode = null,
854 ): iterable {
855 if ($hydrationMode !== null) {
856 $this->setHydrationMode($hydrationMode);
857 }
858
859 if (count($parameters) !== 0) {
860 $this->setParameters($parameters);
861 }
862
863 $rsm = $this->getResultSetMapping();
864 if ($rsm === null) {
865 throw new LogicException('Uninitialized result set mapping.');
866 }
867
868 if ($rsm->isMixed && count($rsm->scalarMappings) > 0) {
869 throw QueryException::iterateWithMixedResultNotAllowed();
870 }
871
872 $stmt = $this->_doExecute();
873
874 return $this->em->newHydrator($this->hydrationMode)->toIterable($stmt, $rsm, $this->hints);
875 }
876
877 /**
878 * Executes the query.
879 *
880 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
881 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
882 */
883 public function execute(
884 ArrayCollection|array|null $parameters = null,
885 string|int|null $hydrationMode = null,
886 ): mixed {
887 if ($this->cacheable && $this->isCacheEnabled()) {
888 return $this->executeUsingQueryCache($parameters, $hydrationMode);
889 }
890
891 return $this->executeIgnoreQueryCache($parameters, $hydrationMode);
892 }
893
894 /**
895 * Execute query ignoring second level cache.
896 *
897 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
898 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
899 */
900 private function executeIgnoreQueryCache(
901 ArrayCollection|array|null $parameters = null,
902 string|int|null $hydrationMode = null,
903 ): mixed {
904 if ($hydrationMode !== null) {
905 $this->setHydrationMode($hydrationMode);
906 }
907
908 if (! empty($parameters)) {
909 $this->setParameters($parameters);
910 }
911
912 $setCacheEntry = static function ($data): void {
913 };
914
915 if ($this->hydrationCacheProfile !== null) {
916 [$cacheKey, $realCacheKey] = $this->getHydrationCacheId();
917
918 $cache = $this->getHydrationCache();
919 $cacheItem = $cache->getItem($cacheKey);
920 $result = $cacheItem->isHit() ? $cacheItem->get() : [];
921
922 if (isset($result[$realCacheKey])) {
923 return $result[$realCacheKey];
924 }
925
926 if (! $result) {
927 $result = [];
928 }
929
930 $setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void {
931 $cache->save($cacheItem->set($result + [$realCacheKey => $data]));
932 };
933 }
934
935 $stmt = $this->_doExecute();
936
937 if (is_numeric($stmt)) {
938 $setCacheEntry($stmt);
939
940 return $stmt;
941 }
942
943 $rsm = $this->getResultSetMapping();
944 if ($rsm === null) {
945 throw new LogicException('Uninitialized result set mapping.');
946 }
947
948 $data = $this->em->newHydrator($this->hydrationMode)->hydrateAll($stmt, $rsm, $this->hints);
949
950 $setCacheEntry($data);
951
952 return $data;
953 }
954
955 private function getHydrationCache(): CacheItemPoolInterface
956 {
957 assert($this->hydrationCacheProfile !== null);
958
959 $cache = $this->hydrationCacheProfile->getResultCache();
960 assert($cache !== null);
961
962 return $cache;
963 }
964
965 /**
966 * Load from second level cache or executes the query and put into cache.
967 *
968 * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters
969 * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode
970 */
971 private function executeUsingQueryCache(
972 ArrayCollection|array|null $parameters = null,
973 string|int|null $hydrationMode = null,
974 ): mixed {
975 $rsm = $this->getResultSetMapping();
976 if ($rsm === null) {
977 throw new LogicException('Uninitialized result set mapping.');
978 }
979
980 $queryCache = $this->em->getCache()->getQueryCache($this->cacheRegion);
981 $queryKey = new QueryCacheKey(
982 $this->getHash(),
983 $this->lifetime,
984 $this->cacheMode ?: Cache::MODE_NORMAL,
985 $this->getTimestampKey(),
986 );
987
988 $result = $queryCache->get($queryKey, $rsm, $this->hints);
989
990 if ($result !== null) {
991 if ($this->cacheLogger) {
992 $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $queryKey);
993 }
994
995 return $result;
996 }
997
998 $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode);
999 $cached = $queryCache->put($queryKey, $rsm, $result, $this->hints);
1000
1001 if ($this->cacheLogger) {
1002 $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $queryKey);
1003
1004 if ($cached) {
1005 $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $queryKey);
1006 }
1007 }
1008
1009 return $result;
1010 }
1011
1012 private function getTimestampKey(): TimestampCacheKey|null
1013 {
1014 assert($this->resultSetMapping !== null);
1015 $entityName = reset($this->resultSetMapping->aliasMap);
1016
1017 if (empty($entityName)) {
1018 return null;
1019 }
1020
1021 $metadata = $this->em->getClassMetadata($entityName);
1022
1023 return new TimestampCacheKey($metadata->rootEntityName);
1024 }
1025
1026 /**
1027 * Get the result cache id to use to store the result set cache entry.
1028 * Will return the configured id if it exists otherwise a hash will be
1029 * automatically generated for you.
1030 *
1031 * @return string[] ($key, $hash)
1032 * @psalm-return array{string, string} ($key, $hash)
1033 */
1034 protected function getHydrationCacheId(): array
1035 {
1036 $parameters = [];
1037 $types = [];
1038
1039 foreach ($this->getParameters() as $parameter) {
1040 $parameters[$parameter->getName()] = $this->processParameterValue($parameter->getValue());
1041 $types[$parameter->getName()] = $parameter->getType();
1042 }
1043
1044 $sql = $this->getSQL();
1045 assert(is_string($sql));
1046 $queryCacheProfile = $this->getHydrationCacheProfile();
1047 $hints = $this->getHints();
1048 $hints['hydrationMode'] = $this->getHydrationMode();
1049
1050 ksort($hints);
1051 assert($queryCacheProfile !== null);
1052
1053 return $queryCacheProfile->generateCacheKeys($sql, $parameters, $types, $hints);
1054 }
1055
1056 /**
1057 * Set the result cache id to use to store the result set cache entry.
1058 * If this is not explicitly set by the developer then a hash is automatically
1059 * generated for you.
1060 */
1061 public function setResultCacheId(string|null $id): static
1062 {
1063 if (! $this->queryCacheProfile) {
1064 return $this->setResultCacheProfile(new QueryCacheProfile(0, $id));
1065 }
1066
1067 $this->queryCacheProfile = $this->queryCacheProfile->setCacheKey($id);
1068
1069 return $this;
1070 }
1071
1072 /**
1073 * Executes the query and returns a the resulting Statement object.
1074 *
1075 * @return Result|int The executed database statement that holds
1076 * the results, or an integer indicating how
1077 * many rows were affected.
1078 */
1079 abstract protected function _doExecute(): Result|int;
1080
1081 /**
1082 * Cleanup Query resource when clone is called.
1083 */
1084 public function __clone()
1085 {
1086 $this->parameters = new ArrayCollection();
1087
1088 $this->hints = [];
1089 $this->hints = $this->em->getConfiguration()->getDefaultQueryHints();
1090 }
1091
1092 /**
1093 * Generates a string of currently query to use for the cache second level cache.
1094 */
1095 protected function getHash(): string
1096 {
1097 $query = $this->getSQL();
1098 assert(is_string($query));
1099 $hints = $this->getHints();
1100 $params = array_map(function (Parameter $parameter) {
1101 $value = $parameter->getValue();
1102
1103 // Small optimization
1104 // Does not invoke processParameterValue for scalar value
1105 if (is_scalar($value)) {
1106 return $value;
1107 }
1108
1109 return $this->processParameterValue($value);
1110 }, $this->parameters->getValues());
1111
1112 ksort($hints);
1113
1114 return sha1($query . '-' . serialize($params) . '-' . serialize($hints));
1115 }
1116}
diff --git a/vendor/doctrine/orm/src/Cache.php b/vendor/doctrine/orm/src/Cache.php
new file mode 100644
index 0000000..8020b27
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache.php
@@ -0,0 +1,106 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Cache\QueryCache;
8use Doctrine\ORM\Cache\Region;
9
10/**
11 * Provides an API for querying/managing the second level cache regions.
12 */
13interface Cache
14{
15 public const DEFAULT_QUERY_REGION_NAME = 'query_cache_region';
16
17 public const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region';
18
19 /**
20 * May read items from the cache, but will not add items.
21 */
22 public const MODE_GET = 1;
23
24 /**
25 * Will never read items from the cache,
26 * but will add items to the cache as it reads them from the database.
27 */
28 public const MODE_PUT = 2;
29
30 /**
31 * May read items from the cache, and add items to the cache.
32 */
33 public const MODE_NORMAL = 3;
34
35 /**
36 * The query will never read items from the cache,
37 * but will refresh items to the cache as it reads them from the database.
38 */
39 public const MODE_REFRESH = 4;
40
41 public function getEntityCacheRegion(string $className): Region|null;
42
43 public function getCollectionCacheRegion(string $className, string $association): Region|null;
44
45 /**
46 * Determine whether the cache contains data for the given entity "instance".
47 */
48 public function containsEntity(string $className, mixed $identifier): bool;
49
50 /**
51 * Evicts the entity data for a particular entity "instance".
52 */
53 public function evictEntity(string $className, mixed $identifier): void;
54
55 /**
56 * Evicts all entity data from the given region.
57 */
58 public function evictEntityRegion(string $className): void;
59
60 /**
61 * Evict data from all entity regions.
62 */
63 public function evictEntityRegions(): void;
64
65 /**
66 * Determine whether the cache contains data for the given collection.
67 */
68 public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool;
69
70 /**
71 * Evicts the cache data for the given identified collection instance.
72 */
73 public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void;
74
75 /**
76 * Evicts all entity data from the given region.
77 */
78 public function evictCollectionRegion(string $className, string $association): void;
79
80 /**
81 * Evict data from all collection regions.
82 */
83 public function evictCollectionRegions(): void;
84
85 /**
86 * Determine whether the cache contains data for the given query.
87 */
88 public function containsQuery(string $regionName): bool;
89
90 /**
91 * Evicts all cached query results under the given name, or default query cache if the region name is NULL.
92 */
93 public function evictQueryRegion(string|null $regionName = null): void;
94
95 /**
96 * Evict data from all query regions.
97 */
98 public function evictQueryRegions(): void;
99
100 /**
101 * Get query cache by region name or create a new one if none exist.
102 *
103 * @param string|null $regionName Query cache region name, or default query cache if the region name is NULL.
104 */
105 public function getQueryCache(string|null $regionName = null): QueryCache;
106}
diff --git a/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php b/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php
new file mode 100644
index 0000000..7dc1fbe
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php
@@ -0,0 +1,30 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7class AssociationCacheEntry implements CacheEntry
8{
9 /**
10 * @param array<string, mixed> $identifier The entity identifier.
11 * @param class-string $class The entity class name
12 */
13 public function __construct(
14 public readonly string $class,
15 public readonly array $identifier,
16 ) {
17 }
18
19 /**
20 * Creates a new AssociationCacheEntry
21 *
22 * This method allow Doctrine\Common\Cache\PhpFileCache compatibility
23 *
24 * @param array<string, mixed> $values array containing property values
25 */
26 public static function __set_state(array $values): self
27 {
28 return new self($values['class'], $values['identifier']);
29 }
30}
diff --git a/vendor/doctrine/orm/src/Cache/CacheConfiguration.php b/vendor/doctrine/orm/src/Cache/CacheConfiguration.php
new file mode 100644
index 0000000..0f8dea7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CacheConfiguration.php
@@ -0,0 +1,60 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache\Logging\CacheLogger;
8
9/**
10 * Configuration container for second-level cache.
11 */
12class CacheConfiguration
13{
14 private CacheFactory|null $cacheFactory = null;
15 private RegionsConfiguration|null $regionsConfig = null;
16 private CacheLogger|null $cacheLogger = null;
17 private QueryCacheValidator|null $queryValidator = null;
18
19 public function getCacheFactory(): CacheFactory|null
20 {
21 return $this->cacheFactory;
22 }
23
24 public function setCacheFactory(CacheFactory $factory): void
25 {
26 $this->cacheFactory = $factory;
27 }
28
29 public function getCacheLogger(): CacheLogger|null
30 {
31 return $this->cacheLogger;
32 }
33
34 public function setCacheLogger(CacheLogger $logger): void
35 {
36 $this->cacheLogger = $logger;
37 }
38
39 public function getRegionsConfiguration(): RegionsConfiguration
40 {
41 return $this->regionsConfig ??= new RegionsConfiguration();
42 }
43
44 public function setRegionsConfiguration(RegionsConfiguration $regionsConfig): void
45 {
46 $this->regionsConfig = $regionsConfig;
47 }
48
49 public function getQueryValidator(): QueryCacheValidator
50 {
51 return $this->queryValidator ??= new TimestampQueryCacheValidator(
52 $this->cacheFactory->getTimestampRegion(),
53 );
54 }
55
56 public function setQueryValidator(QueryCacheValidator $validator): void
57 {
58 $this->queryValidator = $validator;
59 }
60}
diff --git a/vendor/doctrine/orm/src/Cache/CacheEntry.php b/vendor/doctrine/orm/src/Cache/CacheEntry.php
new file mode 100644
index 0000000..6e12de1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CacheEntry.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Cache entry interface
9 *
10 * <b>IMPORTANT NOTE:</b>
11 *
12 * Fields of classes that implement CacheEntry are public for performance reason.
13 */
14interface CacheEntry
15{
16}
diff --git a/vendor/doctrine/orm/src/Cache/CacheException.php b/vendor/doctrine/orm/src/Cache/CacheException.php
new file mode 100644
index 0000000..b422095
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CacheException.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10use function sprintf;
11
12/**
13 * Exception for cache.
14 */
15class CacheException extends LogicException implements ORMException
16{
17 public static function updateReadOnlyCollection(string $sourceEntity, string $fieldName): self
18 {
19 return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName));
20 }
21
22 public static function nonCacheableEntity(string $entityName): self
23 {
24 return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName));
25 }
26}
diff --git a/vendor/doctrine/orm/src/Cache/CacheFactory.php b/vendor/doctrine/orm/src/Cache/CacheFactory.php
new file mode 100644
index 0000000..b15c23e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CacheFactory.php
@@ -0,0 +1,64 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister;
9use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
10use Doctrine\ORM\EntityManagerInterface;
11use Doctrine\ORM\Mapping\AssociationMapping;
12use Doctrine\ORM\Mapping\ClassMetadata;
13use Doctrine\ORM\Persisters\Collection\CollectionPersister;
14use Doctrine\ORM\Persisters\Entity\EntityPersister;
15
16/**
17 * Contract for building second level cache regions components.
18 */
19interface CacheFactory
20{
21 /**
22 * Build an entity persister for the given entity metadata.
23 */
24 public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister;
25
26 /** Build a collection persister for the given relation mapping. */
27 public function buildCachedCollectionPersister(
28 EntityManagerInterface $em,
29 CollectionPersister $persister,
30 AssociationMapping $mapping,
31 ): CachedCollectionPersister;
32
33 /**
34 * Build a query cache based on the given region name
35 */
36 public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache;
37
38 /**
39 * Build an entity hydrator
40 */
41 public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator;
42
43 /**
44 * Build a collection hydrator
45 */
46 public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator;
47
48 /**
49 * Build a cache region
50 *
51 * @param array<string,mixed> $cache The cache configuration.
52 */
53 public function getRegion(array $cache): Region;
54
55 /**
56 * Build timestamp cache region
57 */
58 public function getTimestampRegion(): TimestampRegion;
59
60 /**
61 * Build \Doctrine\ORM\Cache
62 */
63 public function createCache(EntityManagerInterface $entityManager): Cache;
64}
diff --git a/vendor/doctrine/orm/src/Cache/CacheKey.php b/vendor/doctrine/orm/src/Cache/CacheKey.php
new file mode 100644
index 0000000..970702c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CacheKey.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Defines entity / collection / query key to be stored in the cache region.
9 * Allows multiple roles to be stored in the same cache region.
10 */
11abstract class CacheKey
12{
13 public function __construct(public readonly string $hash)
14 {
15 }
16}
diff --git a/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php b/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php
new file mode 100644
index 0000000..fde4575
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7class CollectionCacheEntry implements CacheEntry
8{
9 /** @param CacheKey[] $identifiers List of entity identifiers hold by the collection */
10 public function __construct(public readonly array $identifiers)
11 {
12 }
13
14 /**
15 * Creates a new CollectionCacheEntry
16 *
17 * This method allows for Doctrine\Common\Cache\PhpFileCache compatibility
18 *
19 * @param array<string, mixed> $values array containing property values
20 */
21 public static function __set_state(array $values): CollectionCacheEntry
22 {
23 return new self($values['identifiers']);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php b/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php
new file mode 100644
index 0000000..51b408f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php
@@ -0,0 +1,39 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function implode;
8use function ksort;
9use function str_replace;
10use function strtolower;
11
12/**
13 * Defines entity collection roles to be stored in the cache region.
14 */
15class CollectionCacheKey extends CacheKey
16{
17 /**
18 * The owner entity identifier
19 *
20 * @var array<string, mixed>
21 */
22 public readonly array $ownerIdentifier;
23
24 /**
25 * @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
26 * @param class-string $entityClass The owner entity class
27 */
28 public function __construct(
29 public readonly string $entityClass,
30 public readonly string $association,
31 array $ownerIdentifier,
32 ) {
33 ksort($ownerIdentifier);
34
35 $this->ownerIdentifier = $ownerIdentifier;
36
37 parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
38 }
39}
diff --git a/vendor/doctrine/orm/src/Cache/CollectionHydrator.php b/vendor/doctrine/orm/src/Cache/CollectionHydrator.php
new file mode 100644
index 0000000..16a6572
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/CollectionHydrator.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\Common\Collections\Collection;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\PersistentCollection;
10
11/**
12 * Hydrator cache entry for collections
13 */
14interface CollectionHydrator
15{
16 /** @param mixed[]|Collection $collection The collection. */
17 public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry;
18
19 /** @return mixed[]|null */
20 public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null;
21}
diff --git a/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php b/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php
new file mode 100644
index 0000000..e9ca870
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php
@@ -0,0 +1,36 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Defines contract for concurrently managed data region.
9 * It should be able to lock an specific cache entry in an atomic operation.
10 *
11 * When a entry is locked another process should not be able to read or write the entry.
12 * All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock.
13 */
14interface ConcurrentRegion extends Region
15{
16 /**
17 * Attempts to read lock the mapping for the given key.
18 *
19 * @param CacheKey $key The key of the item to lock.
20 *
21 * @return Lock|null A lock instance or NULL if the lock already exists.
22 *
23 * @throws LockException Indicates a problem accessing the region.
24 */
25 public function lock(CacheKey $key): Lock|null;
26
27 /**
28 * Attempts to read unlock the mapping for the given key.
29 *
30 * @param CacheKey $key The key of the item to unlock.
31 * @param Lock $lock The lock previously obtained from {@link readLock}
32 *
33 * @throws LockException Indicates a problem accessing the region.
34 */
35 public function unlock(CacheKey $key, Lock $lock): bool;
36}
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCache.php b/vendor/doctrine/orm/src/Cache/DefaultCache.php
new file mode 100644
index 0000000..685181c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/DefaultCache.php
@@ -0,0 +1,245 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Cache\Persister\CachedPersister;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\ORMInvalidArgumentException;
12use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
13use Doctrine\ORM\UnitOfWork;
14
15use function is_array;
16use function is_object;
17
18/**
19 * Provides an API for querying/managing the second level cache regions.
20 */
21class DefaultCache implements Cache
22{
23 private readonly UnitOfWork $uow;
24 private readonly CacheFactory $cacheFactory;
25
26 /**
27 * @var QueryCache[]
28 * @psalm-var array<string, QueryCache>
29 */
30 private array $queryCaches = [];
31
32 private QueryCache|null $defaultQueryCache = null;
33
34 public function __construct(
35 private readonly EntityManagerInterface $em,
36 ) {
37 $this->uow = $em->getUnitOfWork();
38 $this->cacheFactory = $em->getConfiguration()
39 ->getSecondLevelCacheConfiguration()
40 ->getCacheFactory();
41 }
42
43 public function getEntityCacheRegion(string $className): Region|null
44 {
45 $metadata = $this->em->getClassMetadata($className);
46 $persister = $this->uow->getEntityPersister($metadata->rootEntityName);
47
48 if (! ($persister instanceof CachedPersister)) {
49 return null;
50 }
51
52 return $persister->getCacheRegion();
53 }
54
55 public function getCollectionCacheRegion(string $className, string $association): Region|null
56 {
57 $metadata = $this->em->getClassMetadata($className);
58 $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
59
60 if (! ($persister instanceof CachedPersister)) {
61 return null;
62 }
63
64 return $persister->getCacheRegion();
65 }
66
67 public function containsEntity(string $className, mixed $identifier): bool
68 {
69 $metadata = $this->em->getClassMetadata($className);
70 $persister = $this->uow->getEntityPersister($metadata->rootEntityName);
71
72 if (! ($persister instanceof CachedPersister)) {
73 return false;
74 }
75
76 return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier));
77 }
78
79 public function evictEntity(string $className, mixed $identifier): void
80 {
81 $metadata = $this->em->getClassMetadata($className);
82 $persister = $this->uow->getEntityPersister($metadata->rootEntityName);
83
84 if (! ($persister instanceof CachedPersister)) {
85 return;
86 }
87
88 $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier));
89 }
90
91 public function evictEntityRegion(string $className): void
92 {
93 $metadata = $this->em->getClassMetadata($className);
94 $persister = $this->uow->getEntityPersister($metadata->rootEntityName);
95
96 if (! ($persister instanceof CachedPersister)) {
97 return;
98 }
99
100 $persister->getCacheRegion()->evictAll();
101 }
102
103 public function evictEntityRegions(): void
104 {
105 $metadatas = $this->em->getMetadataFactory()->getAllMetadata();
106
107 foreach ($metadatas as $metadata) {
108 $persister = $this->uow->getEntityPersister($metadata->rootEntityName);
109
110 if (! ($persister instanceof CachedPersister)) {
111 continue;
112 }
113
114 $persister->getCacheRegion()->evictAll();
115 }
116 }
117
118 public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool
119 {
120 $metadata = $this->em->getClassMetadata($className);
121 $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
122
123 if (! ($persister instanceof CachedPersister)) {
124 return false;
125 }
126
127 return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
128 }
129
130 public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void
131 {
132 $metadata = $this->em->getClassMetadata($className);
133 $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
134
135 if (! ($persister instanceof CachedPersister)) {
136 return;
137 }
138
139 $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
140 }
141
142 public function evictCollectionRegion(string $className, string $association): void
143 {
144 $metadata = $this->em->getClassMetadata($className);
145 $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
146
147 if (! ($persister instanceof CachedPersister)) {
148 return;
149 }
150
151 $persister->getCacheRegion()->evictAll();
152 }
153
154 public function evictCollectionRegions(): void
155 {
156 $metadatas = $this->em->getMetadataFactory()->getAllMetadata();
157
158 foreach ($metadatas as $metadata) {
159 foreach ($metadata->associationMappings as $association) {
160 if (! $association->isToMany()) {
161 continue;
162 }
163
164 $persister = $this->uow->getCollectionPersister($association);
165
166 if (! ($persister instanceof CachedPersister)) {
167 continue;
168 }
169
170 $persister->getCacheRegion()->evictAll();
171 }
172 }
173 }
174
175 public function containsQuery(string $regionName): bool
176 {
177 return isset($this->queryCaches[$regionName]);
178 }
179
180 public function evictQueryRegion(string|null $regionName = null): void
181 {
182 if ($regionName === null && $this->defaultQueryCache !== null) {
183 $this->defaultQueryCache->clear();
184
185 return;
186 }
187
188 if (isset($this->queryCaches[$regionName])) {
189 $this->queryCaches[$regionName]->clear();
190 }
191 }
192
193 public function evictQueryRegions(): void
194 {
195 $this->getQueryCache()->clear();
196
197 foreach ($this->queryCaches as $queryCache) {
198 $queryCache->clear();
199 }
200 }
201
202 public function getQueryCache(string|null $regionName = null): QueryCache
203 {
204 if ($regionName === null) {
205 return $this->defaultQueryCache ??= $this->cacheFactory->buildQueryCache($this->em);
206 }
207
208 return $this->queryCaches[$regionName] ??= $this->cacheFactory->buildQueryCache($this->em, $regionName);
209 }
210
211 private function buildEntityCacheKey(ClassMetadata $metadata, mixed $identifier): EntityCacheKey
212 {
213 if (! is_array($identifier)) {
214 $identifier = $this->toIdentifierArray($metadata, $identifier);
215 }
216
217 return new EntityCacheKey($metadata->rootEntityName, $identifier);
218 }
219
220 private function buildCollectionCacheKey(
221 ClassMetadata $metadata,
222 string $association,
223 mixed $ownerIdentifier,
224 ): CollectionCacheKey {
225 if (! is_array($ownerIdentifier)) {
226 $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);
227 }
228
229 return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier);
230 }
231
232 /** @return array<string, mixed> */
233 private function toIdentifierArray(ClassMetadata $metadata, mixed $identifier): array
234 {
235 if (is_object($identifier)) {
236 $class = DefaultProxyClassNameResolver::getClass($identifier);
237 if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
238 $identifier = $this->uow->getSingleIdentifierValue($identifier)
239 ?? throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
240 }
241 }
242
243 return [$metadata->identifier[0] => $identifier];
244 }
245}
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php b/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php
new file mode 100644
index 0000000..84ea490
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php
@@ -0,0 +1,189 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister;
9use Doctrine\ORM\Cache\Persister\Collection\NonStrictReadWriteCachedCollectionPersister;
10use Doctrine\ORM\Cache\Persister\Collection\ReadOnlyCachedCollectionPersister;
11use Doctrine\ORM\Cache\Persister\Collection\ReadWriteCachedCollectionPersister;
12use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
13use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister;
14use Doctrine\ORM\Cache\Persister\Entity\ReadOnlyCachedEntityPersister;
15use Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister;
16use Doctrine\ORM\Cache\Region\DefaultRegion;
17use Doctrine\ORM\Cache\Region\FileLockRegion;
18use Doctrine\ORM\Cache\Region\UpdateTimestampCache;
19use Doctrine\ORM\EntityManagerInterface;
20use Doctrine\ORM\Mapping\AssociationMapping;
21use Doctrine\ORM\Mapping\ClassMetadata;
22use Doctrine\ORM\Persisters\Collection\CollectionPersister;
23use Doctrine\ORM\Persisters\Entity\EntityPersister;
24use InvalidArgumentException;
25use LogicException;
26use Psr\Cache\CacheItemPoolInterface;
27
28use function assert;
29use function sprintf;
30
31use const DIRECTORY_SEPARATOR;
32
33class DefaultCacheFactory implements CacheFactory
34{
35 private TimestampRegion|null $timestampRegion = null;
36
37 /** @var Region[] */
38 private array $regions = [];
39
40 private string|null $fileLockRegionDirectory = null;
41
42 public function __construct(private readonly RegionsConfiguration $regionsConfig, private readonly CacheItemPoolInterface $cacheItemPool)
43 {
44 }
45
46 public function setFileLockRegionDirectory(string $fileLockRegionDirectory): void
47 {
48 $this->fileLockRegionDirectory = $fileLockRegionDirectory;
49 }
50
51 public function getFileLockRegionDirectory(): string|null
52 {
53 return $this->fileLockRegionDirectory;
54 }
55
56 public function setRegion(Region $region): void
57 {
58 $this->regions[$region->getName()] = $region;
59 }
60
61 public function setTimestampRegion(TimestampRegion $region): void
62 {
63 $this->timestampRegion = $region;
64 }
65
66 public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister
67 {
68 assert($metadata->cache !== null);
69 $region = $this->getRegion($metadata->cache);
70 $usage = $metadata->cache['usage'];
71
72 if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
73 return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata);
74 }
75
76 if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
77 return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
78 }
79
80 if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
81 if (! $region instanceof ConcurrentRegion) {
82 throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
83 }
84
85 return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
86 }
87
88 throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
89 }
90
91 public function buildCachedCollectionPersister(
92 EntityManagerInterface $em,
93 CollectionPersister $persister,
94 AssociationMapping $mapping,
95 ): CachedCollectionPersister {
96 assert(isset($mapping->cache));
97 $usage = $mapping->cache['usage'];
98 $region = $this->getRegion($mapping->cache);
99
100 if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
101 return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping);
102 }
103
104 if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
105 return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
106 }
107
108 if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
109 if (! $region instanceof ConcurrentRegion) {
110 throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
111 }
112
113 return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
114 }
115
116 throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
117 }
118
119 public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache
120 {
121 return new DefaultQueryCache(
122 $em,
123 $this->getRegion(
124 [
125 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME,
126 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE,
127 ],
128 ),
129 );
130 }
131
132 public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator
133 {
134 return new DefaultCollectionHydrator($em);
135 }
136
137 public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator
138 {
139 return new DefaultEntityHydrator($em);
140 }
141
142 /**
143 * {@inheritDoc}
144 */
145 public function getRegion(array $cache): Region
146 {
147 if (isset($this->regions[$cache['region']])) {
148 return $this->regions[$cache['region']];
149 }
150
151 $name = $cache['region'];
152 $lifetime = $this->regionsConfig->getLifetime($cache['region']);
153 $region = new DefaultRegion($name, $this->cacheItemPool, $lifetime);
154
155 if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) {
156 if (
157 $this->fileLockRegionDirectory === '' ||
158 $this->fileLockRegionDirectory === null
159 ) {
160 throw new LogicException(
161 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' .
162 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you want to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ',
163 );
164 }
165
166 $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region'];
167 $region = new FileLockRegion($region, $directory, (string) $this->regionsConfig->getLockLifetime($cache['region']));
168 }
169
170 return $this->regions[$cache['region']] = $region;
171 }
172
173 public function getTimestampRegion(): TimestampRegion
174 {
175 if ($this->timestampRegion === null) {
176 $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME;
177 $lifetime = $this->regionsConfig->getLifetime($name);
178
179 $this->timestampRegion = new UpdateTimestampCache($name, $this->cacheItemPool, $lifetime);
180 }
181
182 return $this->timestampRegion;
183 }
184
185 public function createCache(EntityManagerInterface $entityManager): Cache
186 {
187 return new DefaultCache($entityManager);
188 }
189}
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php b/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php
new file mode 100644
index 0000000..249d48f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php
@@ -0,0 +1,75 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\Common\Collections\Collection;
8use Doctrine\ORM\Cache\Persister\CachedPersister;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\PersistentCollection;
12use Doctrine\ORM\Query;
13use Doctrine\ORM\UnitOfWork;
14
15use function assert;
16
17/**
18 * Default hydrator cache for collections
19 */
20class DefaultCollectionHydrator implements CollectionHydrator
21{
22 private readonly UnitOfWork $uow;
23
24 /** @var array<string,mixed> */
25 private static array $hints = [Query::HINT_CACHE_ENABLED => true];
26
27 public function __construct(
28 private readonly EntityManagerInterface $em,
29 ) {
30 $this->uow = $em->getUnitOfWork();
31 }
32
33 public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry
34 {
35 $data = [];
36
37 foreach ($collection as $index => $entity) {
38 $data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity));
39 }
40
41 return new CollectionCacheEntry($data);
42 }
43
44 public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null
45 {
46 $assoc = $metadata->associationMappings[$key->association];
47 $targetPersister = $this->uow->getEntityPersister($assoc->targetEntity);
48 assert($targetPersister instanceof CachedPersister);
49 $targetRegion = $targetPersister->getCacheRegion();
50 $list = [];
51
52 /** @var EntityCacheEntry[]|null $entityEntries */
53 $entityEntries = $targetRegion->getMultiple($entry);
54
55 if ($entityEntries === null) {
56 return null;
57 }
58
59 foreach ($entityEntries as $index => $entityEntry) {
60 $entity = $this->uow->createEntity(
61 $entityEntry->class,
62 $entityEntry->resolveAssociationEntries($this->em),
63 self::$hints,
64 );
65
66 $collection->hydrateSet($index, $entity);
67
68 $list[$index] = $entity;
69 }
70
71 $this->uow->hydrationComplete();
72
73 return $list;
74 }
75}
diff --git a/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php b/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php
new file mode 100644
index 0000000..6bd1524
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php
@@ -0,0 +1,176 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
10use Doctrine\ORM\Query;
11use Doctrine\ORM\UnitOfWork;
12use Doctrine\ORM\Utility\IdentifierFlattener;
13
14use function assert;
15use function is_array;
16use function is_object;
17use function reset;
18
19/**
20 * Default hydrator cache for entities
21 */
22class DefaultEntityHydrator implements EntityHydrator
23{
24 private readonly UnitOfWork $uow;
25 private readonly IdentifierFlattener $identifierFlattener;
26
27 /** @var array<string,mixed> */
28 private static array $hints = [Query::HINT_CACHE_ENABLED => true];
29
30 public function __construct(
31 private readonly EntityManagerInterface $em,
32 ) {
33 $this->uow = $em->getUnitOfWork();
34 $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
35 }
36
37 public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry
38 {
39 $data = $this->uow->getOriginalEntityData($entity);
40 $data = [...$data, ...$metadata->getIdentifierValues($entity)]; // why update has no identifier values ?
41
42 if ($metadata->requiresFetchAfterChange) {
43 if ($metadata->isVersioned) {
44 assert($metadata->versionField !== null);
45 $data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
46 }
47
48 foreach ($metadata->fieldMappings as $name => $fieldMapping) {
49 if (isset($fieldMapping->generated)) {
50 $data[$name] = $metadata->getFieldValue($entity, $name);
51 }
52 }
53 }
54
55 foreach ($metadata->associationMappings as $name => $assoc) {
56 if (! isset($data[$name])) {
57 continue;
58 }
59
60 if (! $assoc->isToOne()) {
61 unset($data[$name]);
62
63 continue;
64 }
65
66 if (! isset($assoc->cache)) {
67 $targetClassMetadata = $this->em->getClassMetadata($assoc->targetEntity);
68 $owningAssociation = $this->em->getMetadataFactory()->getOwningSide($assoc);
69 $associationIds = $this->identifierFlattener->flattenIdentifier(
70 $targetClassMetadata,
71 $targetClassMetadata->getIdentifierValues($data[$name]),
72 );
73
74 unset($data[$name]);
75
76 foreach ($associationIds as $fieldName => $fieldValue) {
77 if (isset($targetClassMetadata->fieldMappings[$fieldName])) {
78 assert($owningAssociation->isToOneOwningSide());
79 $fieldMapping = $targetClassMetadata->fieldMappings[$fieldName];
80
81 $data[$owningAssociation->targetToSourceKeyColumns[$fieldMapping->columnName]] = $fieldValue;
82
83 continue;
84 }
85
86 $targetAssoc = $targetClassMetadata->associationMappings[$fieldName];
87
88 assert($assoc->isToOneOwningSide());
89 foreach ($assoc->targetToSourceKeyColumns as $referencedColumn => $localColumn) {
90 if (isset($targetAssoc->sourceToTargetKeyColumns[$referencedColumn])) {
91 $data[$localColumn] = $fieldValue;
92 }
93 }
94 }
95
96 continue;
97 }
98
99 if (! isset($assoc->id)) {
100 $targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
101 $targetId = $this->uow->getEntityIdentifier($data[$name]);
102 $data[$name] = new AssociationCacheEntry($targetClass, $targetId);
103
104 continue;
105 }
106
107 // handle association identifier
108 $targetId = is_object($data[$name]) && $this->uow->isInIdentityMap($data[$name])
109 ? $this->uow->getEntityIdentifier($data[$name])
110 : $data[$name];
111
112 // @TODO - fix it !
113 // handle UnitOfWork#createEntity hash generation
114 if (! is_array($targetId)) {
115 assert($assoc->isToOneOwningSide());
116 $data[reset($assoc->joinColumnFieldNames)] = $targetId;
117
118 $targetEntity = $this->em->getClassMetadata($assoc->targetEntity);
119 $targetId = [$targetEntity->identifier[0] => $targetId];
120 }
121
122 $data[$name] = new AssociationCacheEntry($assoc->targetEntity, $targetId);
123 }
124
125 return new EntityCacheEntry($metadata->name, $data);
126 }
127
128 public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null
129 {
130 $data = $entry->data;
131 $hints = self::$hints;
132
133 if ($entity !== null) {
134 $hints[Query::HINT_REFRESH] = true;
135 $hints[Query::HINT_REFRESH_ENTITY] = $entity;
136 }
137
138 foreach ($metadata->associationMappings as $name => $assoc) {
139 if (! isset($assoc->cache) || ! isset($data[$name])) {
140 continue;
141 }
142
143 $assocClass = $data[$name]->class;
144 $assocId = $data[$name]->identifier;
145 $isEagerLoad = ($assoc->fetch === ClassMetadata::FETCH_EAGER || ($assoc->isOneToOne() && ! $assoc->isOwningSide()));
146
147 if (! $isEagerLoad) {
148 $data[$name] = $this->em->getReference($assocClass, $assocId);
149
150 continue;
151 }
152
153 $assocMetadata = $this->em->getClassMetadata($assoc->targetEntity);
154 $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
155 $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
156 $assocRegion = $assocPersister->getCacheRegion();
157 $assocEntry = $assocRegion->get($assocKey);
158
159 if ($assocEntry === null) {
160 return null;
161 }
162
163 $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), $hints);
164 }
165
166 if ($entity !== null) {
167 $this->uow->registerManaged($entity, $key->identifier, $data);
168 }
169
170 $result = $this->uow->createEntity($entry->class, $data, $hints);
171
172 $this->uow->hydrationComplete();
173
174 return $result;
175 }
176}
diff --git a/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php b/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php
new file mode 100644
index 0000000..f3bb8ac
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php
@@ -0,0 +1,414 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\Common\Collections\ArrayCollection;
8use Doctrine\ORM\Cache;
9use Doctrine\ORM\Cache\Exception\FeatureNotImplemented;
10use Doctrine\ORM\Cache\Exception\NonCacheableEntity;
11use Doctrine\ORM\Cache\Logging\CacheLogger;
12use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
13use Doctrine\ORM\EntityManagerInterface;
14use Doctrine\ORM\Mapping\AssociationMapping;
15use Doctrine\ORM\Mapping\ClassMetadata;
16use Doctrine\ORM\PersistentCollection;
17use Doctrine\ORM\Query;
18use Doctrine\ORM\Query\ResultSetMapping;
19use Doctrine\ORM\UnitOfWork;
20
21use function array_map;
22use function array_shift;
23use function array_unshift;
24use function assert;
25use function count;
26use function is_array;
27use function key;
28use function reset;
29
30/**
31 * Default query cache implementation.
32 */
33class DefaultQueryCache implements QueryCache
34{
35 private readonly UnitOfWork $uow;
36 private readonly QueryCacheValidator $validator;
37 protected CacheLogger|null $cacheLogger = null;
38
39 /** @var array<string,mixed> */
40 private static array $hints = [Query::HINT_CACHE_ENABLED => true];
41
42 public function __construct(
43 private readonly EntityManagerInterface $em,
44 private readonly Region $region,
45 ) {
46 $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
47
48 $this->uow = $em->getUnitOfWork();
49 $this->cacheLogger = $cacheConfig->getCacheLogger();
50 $this->validator = $cacheConfig->getQueryValidator();
51 }
52
53 /**
54 * {@inheritDoc}
55 */
56 public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null
57 {
58 if (! ($key->cacheMode & Cache::MODE_GET)) {
59 return null;
60 }
61
62 $cacheEntry = $this->region->get($key);
63
64 if (! $cacheEntry instanceof QueryCacheEntry) {
65 return null;
66 }
67
68 if (! $this->validator->isValid($key, $cacheEntry)) {
69 $this->region->evict($key);
70
71 return null;
72 }
73
74 $result = [];
75 $entityName = reset($rsm->aliasMap);
76 $hasRelation = ! empty($rsm->relationMap);
77 $persister = $this->uow->getEntityPersister($entityName);
78 assert($persister instanceof CachedEntityPersister);
79
80 $region = $persister->getCacheRegion();
81 $regionName = $region->getName();
82
83 $cm = $this->em->getClassMetadata($entityName);
84
85 $generateKeys = static fn (array $entry): EntityCacheKey => new EntityCacheKey($cm->rootEntityName, $entry['identifier']);
86
87 $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
88 $entries = $region->getMultiple($cacheKeys) ?? [];
89
90 // @TODO - move to cache hydration component
91 foreach ($cacheEntry->result as $index => $entry) {
92 $entityEntry = $entries[$index] ?? null;
93
94 if (! $entityEntry instanceof EntityCacheEntry) {
95 $this->cacheLogger?->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
96
97 return null;
98 }
99
100 $this->cacheLogger?->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
101
102 if (! $hasRelation) {
103 $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
104
105 continue;
106 }
107
108 $data = $entityEntry->data;
109
110 foreach ($entry['associations'] as $name => $assoc) {
111 $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
112 assert($assocPersister instanceof CachedEntityPersister);
113
114 $assocRegion = $assocPersister->getCacheRegion();
115 $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
116
117 if ($assoc['type'] & ClassMetadata::TO_ONE) {
118 $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']);
119 $assocEntry = $assocRegion->get($assocKey);
120
121 if ($assocEntry === null) {
122 $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKey);
123
124 $this->uow->hydrationComplete();
125
126 return null;
127 }
128
129 $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
130
131 $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKey);
132
133 continue;
134 }
135
136 if (! isset($assoc['list']) || empty($assoc['list'])) {
137 continue;
138 }
139
140 $generateKeys = static fn (array $id): EntityCacheKey => new EntityCacheKey($assocMetadata->rootEntityName, $id);
141
142 $collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection());
143 $assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
144 $assocEntries = $assocRegion->getMultiple($assocKeys);
145
146 foreach ($assoc['list'] as $assocIndex => $assocId) {
147 $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
148
149 if ($assocEntry === null) {
150 $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
151
152 $this->uow->hydrationComplete();
153
154 return null;
155 }
156
157 $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
158
159 $collection->hydrateSet($assocIndex, $element);
160
161 $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
162 }
163
164 $data[$name] = $collection;
165
166 $collection->setInitialized(true);
167 }
168
169 foreach ($data as $fieldName => $unCachedAssociationData) {
170 // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
171 // cache key information in `$cacheEntry` will not contain details
172 // for fields that are associations.
173 //
174 // This means that `$data` keys for some associations that may
175 // actually not be cached will not be converted to actual association
176 // data, yet they contain L2 cache AssociationCacheEntry objects.
177 //
178 // We need to unwrap those associations into proxy references,
179 // since we don't have actual data for them except for identifiers.
180 if ($unCachedAssociationData instanceof AssociationCacheEntry) {
181 $data[$fieldName] = $this->em->getReference(
182 $unCachedAssociationData->class,
183 $unCachedAssociationData->identifier,
184 );
185 }
186 }
187
188 $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
189 }
190
191 $this->uow->hydrationComplete();
192
193 return $result;
194 }
195
196 /**
197 * {@inheritDoc}
198 */
199 public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool
200 {
201 if ($rsm->scalarMappings) {
202 throw FeatureNotImplemented::scalarResults();
203 }
204
205 if (count($rsm->entityMappings) > 1) {
206 throw FeatureNotImplemented::multipleRootEntities();
207 }
208
209 if (! $rsm->isSelect) {
210 throw FeatureNotImplemented::nonSelectStatements();
211 }
212
213 if (! ($key->cacheMode & Cache::MODE_PUT)) {
214 return false;
215 }
216
217 $data = [];
218 $entityName = reset($rsm->aliasMap);
219 $rootAlias = key($rsm->aliasMap);
220 $persister = $this->uow->getEntityPersister($entityName);
221
222 if (! $persister instanceof CachedEntityPersister) {
223 throw NonCacheableEntity::fromEntity($entityName);
224 }
225
226 $region = $persister->getCacheRegion();
227
228 $cm = $this->em->getClassMetadata($entityName);
229 assert($cm instanceof ClassMetadata);
230
231 foreach ($result as $index => $entity) {
232 $identifier = $this->uow->getEntityIdentifier($entity);
233 $entityKey = new EntityCacheKey($cm->rootEntityName, $identifier);
234
235 if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
236 // Cancel put result if entity put fail
237 if (! $persister->storeEntityCache($entity, $entityKey)) {
238 return false;
239 }
240 }
241
242 $data[$index]['identifier'] = $identifier;
243 $data[$index]['associations'] = [];
244
245 // @TODO - move to cache hydration components
246 foreach ($rsm->relationMap as $alias => $name) {
247 $parentAlias = $rsm->parentAliasMap[$alias];
248 $parentClass = $rsm->aliasMap[$parentAlias];
249 $metadata = $this->em->getClassMetadata($parentClass);
250 $assoc = $metadata->associationMappings[$name];
251 $assocValue = $this->getAssociationValue($rsm, $alias, $entity);
252
253 if ($assocValue === null) {
254 continue;
255 }
256
257 // root entity association
258 if ($rootAlias === $parentAlias) {
259 // Cancel put result if association put fail
260 $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue);
261 if ($assocInfo === null) {
262 return false;
263 }
264
265 $data[$index]['associations'][$name] = $assocInfo;
266
267 continue;
268 }
269
270 // store single nested association
271 if (! is_array($assocValue)) {
272 // Cancel put result if association put fail
273 if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
274 return false;
275 }
276
277 continue;
278 }
279
280 // store array of nested association
281 foreach ($assocValue as $aVal) {
282 // Cancel put result if association put fail
283 if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
284 return false;
285 }
286 }
287 }
288 }
289
290 return $this->region->put($key, new QueryCacheEntry($data));
291 }
292
293 /**
294 * @return mixed[]|null
295 * @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null
296 */
297 private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null
298 {
299 $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
300 $assocMetadata = $assocPersister->getClassMetadata();
301 $assocRegion = $assocPersister->getCacheRegion();
302
303 // Handle *-to-one associations
304 if ($assoc->isToOne()) {
305 $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
306 $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
307
308 if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
309 // Entity put fail
310 if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
311 return null;
312 }
313 }
314
315 return [
316 'targetEntity' => $assocMetadata->rootEntityName,
317 'identifier' => $assocIdentifier,
318 'type' => $assoc->type(),
319 ];
320 }
321
322 // Handle *-to-many associations
323 $list = [];
324
325 foreach ($assocValue as $assocItemIndex => $assocItem) {
326 $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
327 $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
328
329 if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
330 // Entity put fail
331 if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
332 return null;
333 }
334 }
335
336 $list[$assocItemIndex] = $assocIdentifier;
337 }
338
339 return [
340 'targetEntity' => $assocMetadata->rootEntityName,
341 'type' => $assoc->type(),
342 'list' => $list,
343 ];
344 }
345
346 /** @psalm-return list<mixed>|object|null */
347 private function getAssociationValue(
348 ResultSetMapping $rsm,
349 string $assocAlias,
350 object $entity,
351 ): array|object|null {
352 $path = [];
353 $alias = $assocAlias;
354
355 while (isset($rsm->parentAliasMap[$alias])) {
356 $parent = $rsm->parentAliasMap[$alias];
357 $field = $rsm->relationMap[$alias];
358 $class = $rsm->aliasMap[$parent];
359
360 array_unshift($path, [
361 'field' => $field,
362 'class' => $class,
363 ]);
364
365 $alias = $parent;
366 }
367
368 return $this->getAssociationPathValue($entity, $path);
369 }
370
371 /**
372 * @psalm-param array<array-key, array{field: string, class: string}> $path
373 *
374 * @psalm-return list<mixed>|object|null
375 */
376 private function getAssociationPathValue(mixed $value, array $path): array|object|null
377 {
378 $mapping = array_shift($path);
379 $metadata = $this->em->getClassMetadata($mapping['class']);
380 $assoc = $metadata->associationMappings[$mapping['field']];
381 $value = $metadata->getFieldValue($value, $mapping['field']);
382
383 if ($value === null) {
384 return null;
385 }
386
387 if ($path === []) {
388 return $value;
389 }
390
391 // Handle *-to-one associations
392 if ($assoc->isToOne()) {
393 return $this->getAssociationPathValue($value, $path);
394 }
395
396 $values = [];
397
398 foreach ($value as $item) {
399 $values[] = $this->getAssociationPathValue($item, $path);
400 }
401
402 return $values;
403 }
404
405 public function clear(): bool
406 {
407 return $this->region->evictAll();
408 }
409
410 public function getRegion(): Region
411 {
412 return $this->region;
413 }
414}
diff --git a/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php b/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php
new file mode 100644
index 0000000..69bc813
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9use function array_map;
10
11class EntityCacheEntry implements CacheEntry
12{
13 /**
14 * @param array<string,mixed> $data The entity map data
15 * @psalm-param class-string $class The entity class name
16 */
17 public function __construct(
18 public readonly string $class,
19 public readonly array $data,
20 ) {
21 }
22
23 /**
24 * Creates a new EntityCacheEntry
25 *
26 * This method allows Doctrine\Common\Cache\PhpFileCache compatibility
27 *
28 * @param array<string,mixed> $values array containing property values
29 */
30 public static function __set_state(array $values): self
31 {
32 return new self($values['class'], $values['data']);
33 }
34
35 /**
36 * Retrieves the entity data resolving cache entries
37 *
38 * @return array<string, mixed>
39 */
40 public function resolveAssociationEntries(EntityManagerInterface $em): array
41 {
42 return array_map(static function ($value) use ($em) {
43 if (! ($value instanceof AssociationCacheEntry)) {
44 return $value;
45 }
46
47 return $em->getReference($value->class, $value->identifier);
48 }, $this->data);
49 }
50}
diff --git a/vendor/doctrine/orm/src/Cache/EntityCacheKey.php b/vendor/doctrine/orm/src/Cache/EntityCacheKey.php
new file mode 100644
index 0000000..095ddaa
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/EntityCacheKey.php
@@ -0,0 +1,38 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function implode;
8use function ksort;
9use function str_replace;
10use function strtolower;
11
12/**
13 * Defines entity classes roles to be stored in the cache region.
14 */
15class EntityCacheKey extends CacheKey
16{
17 /**
18 * The entity identifier
19 *
20 * @var array<string, mixed>
21 */
22 public readonly array $identifier;
23
24 /**
25 * @param class-string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class.
26 * @param array<string, mixed> $identifier The entity identifier
27 */
28 public function __construct(
29 public readonly string $entityClass,
30 array $identifier,
31 ) {
32 ksort($identifier);
33
34 $this->identifier = $identifier;
35
36 parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
37 }
38}
diff --git a/vendor/doctrine/orm/src/Cache/EntityHydrator.php b/vendor/doctrine/orm/src/Cache/EntityHydrator.php
new file mode 100644
index 0000000..13cd21f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/EntityHydrator.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Mapping\ClassMetadata;
8
9/**
10 * Hydrator cache entry for entities
11 */
12interface EntityHydrator
13{
14 /**
15 * @param ClassMetadata $metadata The entity metadata.
16 * @param EntityCacheKey $key The entity cache key.
17 * @param object $entity The entity.
18 */
19 public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry;
20
21 /**
22 * @param ClassMetadata $metadata The entity metadata.
23 * @param EntityCacheKey $key The entity cache key.
24 * @param EntityCacheEntry $entry The entity cache entry.
25 * @param object|null $entity The entity to load the cache into. If not specified, a new entity is created.
26 */
27 public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null;
28}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CacheException.php b/vendor/doctrine/orm/src/Cache/Exception/CacheException.php
new file mode 100644
index 0000000..cae1bde
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/CacheException.php
@@ -0,0 +1,14 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7use Doctrine\ORM\Cache\CacheException as BaseCacheException;
8
9/**
10 * Exception for cache.
11 */
12class CacheException extends BaseCacheException
13{
14}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php
new file mode 100644
index 0000000..7ecb4fe
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7use function sprintf;
8
9class CannotUpdateReadOnlyCollection extends CacheException
10{
11 public static function fromEntityAndField(string $sourceEntity, string $fieldName): self
12 {
13 return new self(sprintf(
14 'Cannot update a readonly collection "%s#%s"',
15 $sourceEntity,
16 $fieldName,
17 ));
18 }
19}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php
new file mode 100644
index 0000000..b945514
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7use function sprintf;
8
9class CannotUpdateReadOnlyEntity extends CacheException
10{
11 public static function fromEntity(string $entityName): self
12 {
13 return new self(sprintf('Cannot update a readonly entity "%s"', $entityName));
14 }
15}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php b/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php
new file mode 100644
index 0000000..8767d57
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7class FeatureNotImplemented extends CacheException
8{
9 public static function scalarResults(): self
10 {
11 return new self('Second level cache does not support scalar results.');
12 }
13
14 public static function multipleRootEntities(): self
15 {
16 return new self('Second level cache does not support multiple root entities.');
17 }
18
19 public static function nonSelectStatements(): self
20 {
21 return new self('Second-level cache query supports only select statements.');
22 }
23}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php
new file mode 100644
index 0000000..4f5da85
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7use function sprintf;
8
9class NonCacheableEntity extends CacheException
10{
11 public static function fromEntity(string $entityName): self
12 {
13 return new self(sprintf(
14 'Entity "%s" not configured as part of the second-level cache.',
15 $entityName,
16 ));
17 }
18}
diff --git a/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php
new file mode 100644
index 0000000..984286f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Exception;
6
7use function sprintf;
8
9class NonCacheableEntityAssociation extends CacheException
10{
11 public static function fromEntityAndField(string $entityName, string $field): self
12 {
13 return new self(sprintf(
14 'Entity association field "%s#%s" not configured as part of the second-level cache.',
15 $entityName,
16 $field,
17 ));
18 }
19}
diff --git a/vendor/doctrine/orm/src/Cache/Lock.php b/vendor/doctrine/orm/src/Cache/Lock.php
new file mode 100644
index 0000000..d6d60a3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Lock.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function time;
8use function uniqid;
9
10class Lock
11{
12 public int $time;
13
14 public function __construct(
15 public string $value,
16 int|null $time = null,
17 ) {
18 $this->time = $time ?? time();
19 }
20
21 public static function createLockRead(): Lock
22 {
23 return new self(uniqid((string) time(), true));
24 }
25}
diff --git a/vendor/doctrine/orm/src/Cache/LockException.php b/vendor/doctrine/orm/src/Cache/LockException.php
new file mode 100644
index 0000000..bb2d4ec
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/LockException.php
@@ -0,0 +1,14 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache\Exception\CacheException;
8
9/**
10 * Lock exception for cache.
11 */
12class LockException extends CacheException
13{
14}
diff --git a/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php b/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php
new file mode 100644
index 0000000..64c97c1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php
@@ -0,0 +1,60 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Logging;
6
7use Doctrine\ORM\Cache\CollectionCacheKey;
8use Doctrine\ORM\Cache\EntityCacheKey;
9use Doctrine\ORM\Cache\QueryCacheKey;
10
11/**
12 * Interface for logging.
13 */
14interface CacheLogger
15{
16 /**
17 * Log an entity put into second level cache.
18 */
19 public function entityCachePut(string $regionName, EntityCacheKey $key): void;
20
21 /**
22 * Log an entity get from second level cache resulted in a hit.
23 */
24 public function entityCacheHit(string $regionName, EntityCacheKey $key): void;
25
26 /**
27 * Log an entity get from second level cache resulted in a miss.
28 */
29 public function entityCacheMiss(string $regionName, EntityCacheKey $key): void;
30
31 /**
32 * Log an entity put into second level cache.
33 */
34 public function collectionCachePut(string $regionName, CollectionCacheKey $key): void;
35
36 /**
37 * Log an entity get from second level cache resulted in a hit.
38 */
39 public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void;
40
41 /**
42 * Log an entity get from second level cache resulted in a miss.
43 */
44 public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void;
45
46 /**
47 * Log a query put into the query cache.
48 */
49 public function queryCachePut(string $regionName, QueryCacheKey $key): void;
50
51 /**
52 * Log a query get from the query cache resulted in a hit.
53 */
54 public function queryCacheHit(string $regionName, QueryCacheKey $key): void;
55
56 /**
57 * Log a query get from the query cache resulted in a miss.
58 */
59 public function queryCacheMiss(string $regionName, QueryCacheKey $key): void;
60}
diff --git a/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php b/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php
new file mode 100644
index 0000000..8eef3b5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php
@@ -0,0 +1,94 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Logging;
6
7use Doctrine\ORM\Cache\CollectionCacheKey;
8use Doctrine\ORM\Cache\EntityCacheKey;
9use Doctrine\ORM\Cache\QueryCacheKey;
10
11class CacheLoggerChain implements CacheLogger
12{
13 /** @var array<string, CacheLogger> */
14 private array $loggers = [];
15
16 public function setLogger(string $name, CacheLogger $logger): void
17 {
18 $this->loggers[$name] = $logger;
19 }
20
21 public function getLogger(string $name): CacheLogger|null
22 {
23 return $this->loggers[$name] ?? null;
24 }
25
26 /** @return array<string, CacheLogger> */
27 public function getLoggers(): array
28 {
29 return $this->loggers;
30 }
31
32 public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void
33 {
34 foreach ($this->loggers as $logger) {
35 $logger->collectionCacheHit($regionName, $key);
36 }
37 }
38
39 public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void
40 {
41 foreach ($this->loggers as $logger) {
42 $logger->collectionCacheMiss($regionName, $key);
43 }
44 }
45
46 public function collectionCachePut(string $regionName, CollectionCacheKey $key): void
47 {
48 foreach ($this->loggers as $logger) {
49 $logger->collectionCachePut($regionName, $key);
50 }
51 }
52
53 public function entityCacheHit(string $regionName, EntityCacheKey $key): void
54 {
55 foreach ($this->loggers as $logger) {
56 $logger->entityCacheHit($regionName, $key);
57 }
58 }
59
60 public function entityCacheMiss(string $regionName, EntityCacheKey $key): void
61 {
62 foreach ($this->loggers as $logger) {
63 $logger->entityCacheMiss($regionName, $key);
64 }
65 }
66
67 public function entityCachePut(string $regionName, EntityCacheKey $key): void
68 {
69 foreach ($this->loggers as $logger) {
70 $logger->entityCachePut($regionName, $key);
71 }
72 }
73
74 public function queryCacheHit(string $regionName, QueryCacheKey $key): void
75 {
76 foreach ($this->loggers as $logger) {
77 $logger->queryCacheHit($regionName, $key);
78 }
79 }
80
81 public function queryCacheMiss(string $regionName, QueryCacheKey $key): void
82 {
83 foreach ($this->loggers as $logger) {
84 $logger->queryCacheMiss($regionName, $key);
85 }
86 }
87
88 public function queryCachePut(string $regionName, QueryCacheKey $key): void
89 {
90 foreach ($this->loggers as $logger) {
91 $logger->queryCachePut($regionName, $key);
92 }
93 }
94}
diff --git a/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php b/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php
new file mode 100644
index 0000000..092104e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php
@@ -0,0 +1,174 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Logging;
6
7use Doctrine\ORM\Cache\CollectionCacheKey;
8use Doctrine\ORM\Cache\EntityCacheKey;
9use Doctrine\ORM\Cache\QueryCacheKey;
10
11use function array_sum;
12
13/**
14 * Provide basic second level cache statistics.
15 */
16class StatisticsCacheLogger implements CacheLogger
17{
18 /** @var array<string, int> */
19 private array $cacheMissCountMap = [];
20
21 /** @var array<string, int> */
22 private array $cacheHitCountMap = [];
23
24 /** @var array<string, int> */
25 private array $cachePutCountMap = [];
26
27 public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void
28 {
29 $this->cacheMissCountMap[$regionName]
30 = ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
31 }
32
33 public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void
34 {
35 $this->cacheHitCountMap[$regionName]
36 = ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
37 }
38
39 public function collectionCachePut(string $regionName, CollectionCacheKey $key): void
40 {
41 $this->cachePutCountMap[$regionName]
42 = ($this->cachePutCountMap[$regionName] ?? 0) + 1;
43 }
44
45 public function entityCacheMiss(string $regionName, EntityCacheKey $key): void
46 {
47 $this->cacheMissCountMap[$regionName]
48 = ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
49 }
50
51 public function entityCacheHit(string $regionName, EntityCacheKey $key): void
52 {
53 $this->cacheHitCountMap[$regionName]
54 = ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
55 }
56
57 public function entityCachePut(string $regionName, EntityCacheKey $key): void
58 {
59 $this->cachePutCountMap[$regionName]
60 = ($this->cachePutCountMap[$regionName] ?? 0) + 1;
61 }
62
63 public function queryCacheHit(string $regionName, QueryCacheKey $key): void
64 {
65 $this->cacheHitCountMap[$regionName]
66 = ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
67 }
68
69 public function queryCacheMiss(string $regionName, QueryCacheKey $key): void
70 {
71 $this->cacheMissCountMap[$regionName]
72 = ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
73 }
74
75 public function queryCachePut(string $regionName, QueryCacheKey $key): void
76 {
77 $this->cachePutCountMap[$regionName]
78 = ($this->cachePutCountMap[$regionName] ?? 0) + 1;
79 }
80
81 /**
82 * Get the number of entries successfully retrieved from cache.
83 *
84 * @param string $regionName The name of the cache region.
85 */
86 public function getRegionHitCount(string $regionName): int
87 {
88 return $this->cacheHitCountMap[$regionName] ?? 0;
89 }
90
91 /**
92 * Get the number of cached entries *not* found in cache.
93 *
94 * @param string $regionName The name of the cache region.
95 */
96 public function getRegionMissCount(string $regionName): int
97 {
98 return $this->cacheMissCountMap[$regionName] ?? 0;
99 }
100
101 /**
102 * Get the number of cacheable entries put in cache.
103 *
104 * @param string $regionName The name of the cache region.
105 */
106 public function getRegionPutCount(string $regionName): int
107 {
108 return $this->cachePutCountMap[$regionName] ?? 0;
109 }
110
111 /** @return array<string, int> */
112 public function getRegionsMiss(): array
113 {
114 return $this->cacheMissCountMap;
115 }
116
117 /** @return array<string, int> */
118 public function getRegionsHit(): array
119 {
120 return $this->cacheHitCountMap;
121 }
122
123 /** @return array<string, int> */
124 public function getRegionsPut(): array
125 {
126 return $this->cachePutCountMap;
127 }
128
129 /**
130 * Clear region statistics
131 *
132 * @param string $regionName The name of the cache region.
133 */
134 public function clearRegionStats(string $regionName): void
135 {
136 $this->cachePutCountMap[$regionName] = 0;
137 $this->cacheHitCountMap[$regionName] = 0;
138 $this->cacheMissCountMap[$regionName] = 0;
139 }
140
141 /**
142 * Clear all statistics
143 */
144 public function clearStats(): void
145 {
146 $this->cachePutCountMap = [];
147 $this->cacheHitCountMap = [];
148 $this->cacheMissCountMap = [];
149 }
150
151 /**
152 * Get the total number of put in cache.
153 */
154 public function getPutCount(): int
155 {
156 return array_sum($this->cachePutCountMap);
157 }
158
159 /**
160 * Get the total number of entries successfully retrieved from cache.
161 */
162 public function getHitCount(): int
163 {
164 return array_sum($this->cacheHitCountMap);
165 }
166
167 /**
168 * Get the total number of cached entries *not* found in cache.
169 */
170 public function getMissCount(): int
171 {
172 return array_sum($this->cacheMissCountMap);
173 }
174}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php b/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php
new file mode 100644
index 0000000..223692c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister;
6
7use Doctrine\ORM\Cache\Region;
8
9/**
10 * Interface for persister that support second level cache.
11 */
12interface CachedPersister
13{
14 /**
15 * Perform whatever processing is encapsulated here after completion of the transaction.
16 */
17 public function afterTransactionComplete(): void;
18
19 /**
20 * Perform whatever processing is encapsulated here after completion of the rolled-back.
21 */
22 public function afterTransactionRolledBack(): void;
23
24 public function getCacheRegion(): Region;
25}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php
new file mode 100644
index 0000000..8c087a8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php
@@ -0,0 +1,168 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Collection;
6
7use Doctrine\Common\Collections\Collection;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\ORM\Cache\CollectionCacheKey;
10use Doctrine\ORM\Cache\CollectionHydrator;
11use Doctrine\ORM\Cache\Logging\CacheLogger;
12use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
13use Doctrine\ORM\Cache\Region;
14use Doctrine\ORM\EntityManagerInterface;
15use Doctrine\ORM\Mapping\AssociationMapping;
16use Doctrine\ORM\Mapping\ClassMetadata;
17use Doctrine\ORM\Mapping\ClassMetadataFactory;
18use Doctrine\ORM\PersistentCollection;
19use Doctrine\ORM\Persisters\Collection\CollectionPersister;
20use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
21use Doctrine\ORM\UnitOfWork;
22
23use function array_values;
24use function assert;
25use function count;
26
27abstract class AbstractCollectionPersister implements CachedCollectionPersister
28{
29 protected UnitOfWork $uow;
30 protected ClassMetadataFactory $metadataFactory;
31 protected ClassMetadata $sourceEntity;
32 protected ClassMetadata $targetEntity;
33
34 /** @var mixed[] */
35 protected array $queuedCache = [];
36
37 protected string $regionName;
38 protected CollectionHydrator $hydrator;
39 protected CacheLogger|null $cacheLogger;
40
41 public function __construct(
42 protected CollectionPersister $persister,
43 protected Region $region,
44 EntityManagerInterface $em,
45 protected AssociationMapping $association,
46 ) {
47 $configuration = $em->getConfiguration();
48 $cacheConfig = $configuration->getSecondLevelCacheConfiguration();
49 $cacheFactory = $cacheConfig->getCacheFactory();
50
51 $this->regionName = $region->getName();
52 $this->uow = $em->getUnitOfWork();
53 $this->metadataFactory = $em->getMetadataFactory();
54 $this->cacheLogger = $cacheConfig->getCacheLogger();
55 $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association);
56 $this->sourceEntity = $em->getClassMetadata($association->sourceEntity);
57 $this->targetEntity = $em->getClassMetadata($association->targetEntity);
58 }
59
60 public function getCacheRegion(): Region
61 {
62 return $this->region;
63 }
64
65 public function getSourceEntityMetadata(): ClassMetadata
66 {
67 return $this->sourceEntity;
68 }
69
70 public function getTargetEntityMetadata(): ClassMetadata
71 {
72 return $this->targetEntity;
73 }
74
75 public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null
76 {
77 $cache = $this->region->get($key);
78
79 if ($cache === null) {
80 return null;
81 }
82
83 return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection);
84 }
85
86 public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void
87 {
88 $associationMapping = $this->sourceEntity->associationMappings[$key->association];
89 $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName);
90 assert($targetPersister instanceof CachedEntityPersister);
91 $targetRegion = $targetPersister->getCacheRegion();
92 $targetHydrator = $targetPersister->getEntityHydrator();
93
94 // Only preserve ordering if association configured it
95 if (! $associationMapping->isIndexed()) {
96 // Elements may be an array or a Collection
97 $elements = array_values($elements instanceof Collection ? $elements->getValues() : $elements);
98 }
99
100 $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements);
101
102 foreach ($entry->identifiers as $index => $entityKey) {
103 if ($targetRegion->contains($entityKey)) {
104 continue;
105 }
106
107 $class = $this->targetEntity;
108 $className = DefaultProxyClassNameResolver::getClass($elements[$index]);
109
110 if ($className !== $this->targetEntity->name) {
111 $class = $this->metadataFactory->getMetadataFor($className);
112 }
113
114 $entity = $elements[$index];
115 $entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity);
116
117 $targetRegion->put($entityKey, $entityEntry);
118 }
119
120 if ($this->region->put($key, $entry)) {
121 $this->cacheLogger?->collectionCachePut($this->regionName, $key);
122 }
123 }
124
125 public function contains(PersistentCollection $collection, object $element): bool
126 {
127 return $this->persister->contains($collection, $element);
128 }
129
130 public function containsKey(PersistentCollection $collection, mixed $key): bool
131 {
132 return $this->persister->containsKey($collection, $key);
133 }
134
135 public function count(PersistentCollection $collection): int
136 {
137 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
138 $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
139 $entry = $this->region->get($key);
140
141 if ($entry !== null) {
142 return count($entry->identifiers);
143 }
144
145 return $this->persister->count($collection);
146 }
147
148 public function get(PersistentCollection $collection, mixed $index): mixed
149 {
150 return $this->persister->get($collection, $index);
151 }
152
153 /**
154 * {@inheritDoc}
155 */
156 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
157 {
158 return $this->persister->slice($collection, $offset, $length);
159 }
160
161 /**
162 * {@inheritDoc}
163 */
164 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
165 {
166 return $this->persister->loadCriteria($collection, $criteria);
167 }
168}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php
new file mode 100644
index 0000000..6b10c80
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php
@@ -0,0 +1,36 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Collection;
6
7use Doctrine\Common\Collections\Collection;
8use Doctrine\ORM\Cache\CollectionCacheKey;
9use Doctrine\ORM\Cache\Persister\CachedPersister;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\PersistentCollection;
12use Doctrine\ORM\Persisters\Collection\CollectionPersister;
13
14/**
15 * Interface for second level cache collection persisters.
16 */
17interface CachedCollectionPersister extends CachedPersister, CollectionPersister
18{
19 public function getSourceEntityMetadata(): ClassMetadata;
20
21 public function getTargetEntityMetadata(): ClassMetadata;
22
23 /**
24 * Loads a collection from cache
25 *
26 * @return mixed[]|null
27 */
28 public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null;
29
30 /**
31 * Stores a collection into cache
32 *
33 * @param mixed[]|Collection $elements
34 */
35 public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void;
36}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php
new file mode 100644
index 0000000..ac861f4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php
@@ -0,0 +1,74 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Collection;
6
7use Doctrine\ORM\Cache\CollectionCacheKey;
8use Doctrine\ORM\PersistentCollection;
9
10use function spl_object_id;
11
12class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister
13{
14 public function afterTransactionComplete(): void
15 {
16 if (isset($this->queuedCache['update'])) {
17 foreach ($this->queuedCache['update'] as $item) {
18 $this->storeCollectionCache($item['key'], $item['list']);
19 }
20 }
21
22 if (isset($this->queuedCache['delete'])) {
23 foreach ($this->queuedCache['delete'] as $key) {
24 $this->region->evict($key);
25 }
26 }
27
28 $this->queuedCache = [];
29 }
30
31 public function afterTransactionRolledBack(): void
32 {
33 $this->queuedCache = [];
34 }
35
36 public function delete(PersistentCollection $collection): void
37 {
38 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
39 $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
40
41 $this->persister->delete($collection);
42
43 $this->queuedCache['delete'][spl_object_id($collection)] = $key;
44 }
45
46 public function update(PersistentCollection $collection): void
47 {
48 $isInitialized = $collection->isInitialized();
49 $isDirty = $collection->isDirty();
50
51 if (! $isInitialized && ! $isDirty) {
52 return;
53 }
54
55 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
56 $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
57
58 // Invalidate non initialized collections OR ordered collection
59 if ($isDirty && ! $isInitialized || $this->association->isOrdered()) {
60 $this->persister->update($collection);
61
62 $this->queuedCache['delete'][spl_object_id($collection)] = $key;
63
64 return;
65 }
66
67 $this->persister->update($collection);
68
69 $this->queuedCache['update'][spl_object_id($collection)] = [
70 'key' => $key,
71 'list' => $collection,
72 ];
73 }
74}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php
new file mode 100644
index 0000000..96e0a4b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Collection;
6
7use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
8use Doctrine\ORM\PersistentCollection;
9use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
10
11class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
12{
13 public function update(PersistentCollection $collection): void
14 {
15 if ($collection->isDirty() && $collection->getSnapshot()) {
16 throw CannotUpdateReadOnlyCollection::fromEntityAndField(
17 DefaultProxyClassNameResolver::getClass($collection->getOwner()),
18 $this->association->fieldName,
19 );
20 }
21
22 parent::update($collection);
23 }
24}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php
new file mode 100644
index 0000000..347a065
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php
@@ -0,0 +1,103 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Collection;
6
7use Doctrine\ORM\Cache\CollectionCacheKey;
8use Doctrine\ORM\Cache\ConcurrentRegion;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\AssociationMapping;
11use Doctrine\ORM\PersistentCollection;
12use Doctrine\ORM\Persisters\Collection\CollectionPersister;
13
14use function spl_object_id;
15
16class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
17{
18 public function __construct(
19 CollectionPersister $persister,
20 ConcurrentRegion $region,
21 EntityManagerInterface $em,
22 AssociationMapping $association,
23 ) {
24 parent::__construct($persister, $region, $em, $association);
25 }
26
27 public function afterTransactionComplete(): void
28 {
29 if (isset($this->queuedCache['update'])) {
30 foreach ($this->queuedCache['update'] as $item) {
31 $this->region->evict($item['key']);
32 }
33 }
34
35 if (isset($this->queuedCache['delete'])) {
36 foreach ($this->queuedCache['delete'] as $item) {
37 $this->region->evict($item['key']);
38 }
39 }
40
41 $this->queuedCache = [];
42 }
43
44 public function afterTransactionRolledBack(): void
45 {
46 if (isset($this->queuedCache['update'])) {
47 foreach ($this->queuedCache['update'] as $item) {
48 $this->region->evict($item['key']);
49 }
50 }
51
52 if (isset($this->queuedCache['delete'])) {
53 foreach ($this->queuedCache['delete'] as $item) {
54 $this->region->evict($item['key']);
55 }
56 }
57
58 $this->queuedCache = [];
59 }
60
61 public function delete(PersistentCollection $collection): void
62 {
63 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
64 $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
65 $lock = $this->region->lock($key);
66
67 $this->persister->delete($collection);
68
69 if ($lock === null) {
70 return;
71 }
72
73 $this->queuedCache['delete'][spl_object_id($collection)] = [
74 'key' => $key,
75 'lock' => $lock,
76 ];
77 }
78
79 public function update(PersistentCollection $collection): void
80 {
81 $isInitialized = $collection->isInitialized();
82 $isDirty = $collection->isDirty();
83
84 if (! $isInitialized && ! $isDirty) {
85 return;
86 }
87
88 $this->persister->update($collection);
89
90 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
91 $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId);
92 $lock = $this->region->lock($key);
93
94 if ($lock === null) {
95 return;
96 }
97
98 $this->queuedCache['update'][spl_object_id($collection)] = [
99 'key' => $key,
100 'lock' => $lock,
101 ];
102 }
103}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php
new file mode 100644
index 0000000..9f371d8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php
@@ -0,0 +1,557 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Entity;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\Common\Collections\Order;
9use Doctrine\DBAL\LockMode;
10use Doctrine\ORM\Cache;
11use Doctrine\ORM\Cache\CollectionCacheKey;
12use Doctrine\ORM\Cache\EntityCacheKey;
13use Doctrine\ORM\Cache\EntityHydrator;
14use Doctrine\ORM\Cache\Logging\CacheLogger;
15use Doctrine\ORM\Cache\Persister\CachedPersister;
16use Doctrine\ORM\Cache\QueryCacheKey;
17use Doctrine\ORM\Cache\Region;
18use Doctrine\ORM\Cache\TimestampCacheKey;
19use Doctrine\ORM\Cache\TimestampRegion;
20use Doctrine\ORM\EntityManagerInterface;
21use Doctrine\ORM\Mapping\AssociationMapping;
22use Doctrine\ORM\Mapping\ClassMetadata;
23use Doctrine\ORM\Mapping\ClassMetadataFactory;
24use Doctrine\ORM\PersistentCollection;
25use Doctrine\ORM\Persisters\Entity\EntityPersister;
26use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
27use Doctrine\ORM\Query\ResultSetMapping;
28use Doctrine\ORM\UnitOfWork;
29
30use function array_merge;
31use function assert;
32use function serialize;
33use function sha1;
34
35abstract class AbstractEntityPersister implements CachedEntityPersister
36{
37 protected UnitOfWork $uow;
38 protected ClassMetadataFactory $metadataFactory;
39
40 /** @var mixed[] */
41 protected array $queuedCache = [];
42
43 protected TimestampRegion $timestampRegion;
44 protected TimestampCacheKey $timestampKey;
45 protected EntityHydrator $hydrator;
46 protected Cache $cache;
47 protected CacheLogger|null $cacheLogger = null;
48 protected string $regionName;
49
50 /**
51 * Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
52 *
53 * @var array<string>|null
54 */
55 protected array|null $joinedAssociations = null;
56
57 public function __construct(
58 protected EntityPersister $persister,
59 protected Region $region,
60 EntityManagerInterface $em,
61 protected ClassMetadata $class,
62 ) {
63 $configuration = $em->getConfiguration();
64 $cacheConfig = $configuration->getSecondLevelCacheConfiguration();
65 $cacheFactory = $cacheConfig->getCacheFactory();
66
67 $this->cache = $em->getCache();
68 $this->regionName = $region->getName();
69 $this->uow = $em->getUnitOfWork();
70 $this->metadataFactory = $em->getMetadataFactory();
71 $this->cacheLogger = $cacheConfig->getCacheLogger();
72 $this->timestampRegion = $cacheFactory->getTimestampRegion();
73 $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class);
74 $this->timestampKey = new TimestampCacheKey($this->class->rootEntityName);
75 }
76
77 public function addInsert(object $entity): void
78 {
79 $this->persister->addInsert($entity);
80 }
81
82 /**
83 * {@inheritDoc}
84 */
85 public function getInserts(): array
86 {
87 return $this->persister->getInserts();
88 }
89
90 public function getSelectSQL(
91 array|Criteria $criteria,
92 AssociationMapping|null $assoc = null,
93 LockMode|int|null $lockMode = null,
94 int|null $limit = null,
95 int|null $offset = null,
96 array|null $orderBy = null,
97 ): string {
98 return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy);
99 }
100
101 public function getCountSQL(array|Criteria $criteria = []): string
102 {
103 return $this->persister->getCountSQL($criteria);
104 }
105
106 public function getInsertSQL(): string
107 {
108 return $this->persister->getInsertSQL();
109 }
110
111 public function getResultSetMapping(): ResultSetMapping
112 {
113 return $this->persister->getResultSetMapping();
114 }
115
116 public function getSelectConditionStatementSQL(
117 string $field,
118 mixed $value,
119 AssociationMapping|null $assoc = null,
120 string|null $comparison = null,
121 ): string {
122 return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison);
123 }
124
125 public function exists(object $entity, Criteria|null $extraConditions = null): bool
126 {
127 if ($extraConditions === null) {
128 $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity));
129
130 if ($this->region->contains($key)) {
131 return true;
132 }
133 }
134
135 return $this->persister->exists($entity, $extraConditions);
136 }
137
138 public function getCacheRegion(): Region
139 {
140 return $this->region;
141 }
142
143 public function getEntityHydrator(): EntityHydrator
144 {
145 return $this->hydrator;
146 }
147
148 public function storeEntityCache(object $entity, EntityCacheKey $key): bool
149 {
150 $class = $this->class;
151 $className = DefaultProxyClassNameResolver::getClass($entity);
152
153 if ($className !== $this->class->name) {
154 $class = $this->metadataFactory->getMetadataFor($className);
155 }
156
157 $entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
158 $cached = $this->region->put($key, $entry);
159
160 if ($cached) {
161 $this->cacheLogger?->entityCachePut($this->regionName, $key);
162 }
163
164 return $cached;
165 }
166
167 private function storeJoinedAssociations(object $entity): void
168 {
169 if ($this->joinedAssociations === null) {
170 $associations = [];
171
172 foreach ($this->class->associationMappings as $name => $assoc) {
173 if (
174 isset($assoc->cache) &&
175 ($assoc->isToOne()) &&
176 ($assoc->fetch === ClassMetadata::FETCH_EAGER || ! $assoc->isOwningSide())
177 ) {
178 $associations[] = $name;
179 }
180 }
181
182 $this->joinedAssociations = $associations;
183 }
184
185 foreach ($this->joinedAssociations as $name) {
186 $assoc = $this->class->associationMappings[$name];
187 $assocEntity = $this->class->getFieldValue($entity, $name);
188
189 if ($assocEntity === null) {
190 continue;
191 }
192
193 $assocId = $this->uow->getEntityIdentifier($assocEntity);
194 $assocMetadata = $this->metadataFactory->getMetadataFor($assoc->targetEntity);
195 $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
196 $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
197
198 $assocPersister->storeEntityCache($assocEntity, $assocKey);
199 }
200 }
201
202 /**
203 * Generates a string of currently query
204 *
205 * @param string[]|Criteria $criteria
206 * @param array<string, Order>|null $orderBy
207 */
208 protected function getHash(
209 string $query,
210 array|Criteria $criteria,
211 array|null $orderBy = null,
212 int|null $limit = null,
213 int|null $offset = null,
214 ): string {
215 [$params] = $criteria instanceof Criteria
216 ? $this->persister->expandCriteriaParameters($criteria)
217 : $this->persister->expandParameters($criteria);
218
219 return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
220 }
221
222 /**
223 * {@inheritDoc}
224 */
225 public function expandParameters(array $criteria): array
226 {
227 return $this->persister->expandParameters($criteria);
228 }
229
230 /**
231 * {@inheritDoc}
232 */
233 public function expandCriteriaParameters(Criteria $criteria): array
234 {
235 return $this->persister->expandCriteriaParameters($criteria);
236 }
237
238 public function getClassMetadata(): ClassMetadata
239 {
240 return $this->persister->getClassMetadata();
241 }
242
243 /**
244 * {@inheritDoc}
245 */
246 public function getManyToManyCollection(
247 AssociationMapping $assoc,
248 object $sourceEntity,
249 int|null $offset = null,
250 int|null $limit = null,
251 ): array {
252 return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit);
253 }
254
255 /**
256 * {@inheritDoc}
257 */
258 public function getOneToManyCollection(
259 AssociationMapping $assoc,
260 object $sourceEntity,
261 int|null $offset = null,
262 int|null $limit = null,
263 ): array {
264 return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit);
265 }
266
267 public function getOwningTable(string $fieldName): string
268 {
269 return $this->persister->getOwningTable($fieldName);
270 }
271
272 public function executeInserts(): void
273 {
274 // The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
275 // are performed, so collect all the new entities.
276 $newInserts = $this->persister->getInserts();
277
278 if ($newInserts) {
279 $this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
280 }
281
282 $this->persister->executeInserts();
283 }
284
285 /**
286 * {@inheritDoc}
287 */
288 public function load(
289 array $criteria,
290 object|null $entity = null,
291 AssociationMapping|null $assoc = null,
292 array $hints = [],
293 LockMode|int|null $lockMode = null,
294 int|null $limit = null,
295 array|null $orderBy = null,
296 ): object|null {
297 if ($entity !== null || $assoc !== null || $hints !== [] || $lockMode !== null) {
298 return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
299 }
300
301 //handle only EntityRepository#findOneBy
302 $query = $this->persister->getSelectSQL($criteria, null, null, $limit, null, $orderBy);
303 $hash = $this->getHash($query, $criteria);
304 $rsm = $this->getResultSetMapping();
305 $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
306 $queryCache = $this->cache->getQueryCache($this->regionName);
307 $result = $queryCache->get($queryKey, $rsm);
308
309 if ($result !== null) {
310 $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
311
312 return $result[0];
313 }
314
315 $result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
316
317 if ($result === null) {
318 return null;
319 }
320
321 $cached = $queryCache->put($queryKey, $rsm, [$result]);
322
323 $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
324
325 if ($cached) {
326 $this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
327 }
328
329 return $result;
330 }
331
332 /**
333 * {@inheritDoc}
334 */
335 public function loadAll(
336 array $criteria = [],
337 array|null $orderBy = null,
338 int|null $limit = null,
339 int|null $offset = null,
340 ): array {
341 $query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
342 $hash = $this->getHash($query, $criteria);
343 $rsm = $this->getResultSetMapping();
344 $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
345 $queryCache = $this->cache->getQueryCache($this->regionName);
346 $result = $queryCache->get($queryKey, $rsm);
347
348 if ($result !== null) {
349 $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
350
351 return $result;
352 }
353
354 $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset);
355 $cached = $queryCache->put($queryKey, $rsm, $result);
356
357 if ($result) {
358 $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
359 }
360
361 if ($cached) {
362 $this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
363 }
364
365 return $result;
366 }
367
368 /**
369 * {@inheritDoc}
370 */
371 public function loadById(array $identifier, object|null $entity = null): object|null
372 {
373 $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier);
374 $cacheEntry = $this->region->get($cacheKey);
375 $class = $this->class;
376
377 if ($cacheEntry !== null) {
378 if ($cacheEntry->class !== $this->class->name) {
379 $class = $this->metadataFactory->getMetadataFor($cacheEntry->class);
380 }
381
382 $cachedEntity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity);
383
384 if ($cachedEntity !== null) {
385 $this->cacheLogger?->entityCacheHit($this->regionName, $cacheKey);
386
387 return $cachedEntity;
388 }
389 }
390
391 $entity = $this->persister->loadById($identifier, $entity);
392
393 if ($entity === null) {
394 return null;
395 }
396
397 $class = $this->class;
398 $className = DefaultProxyClassNameResolver::getClass($entity);
399
400 if ($className !== $this->class->name) {
401 $class = $this->metadataFactory->getMetadataFor($className);
402 }
403
404 $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity);
405 $cached = $this->region->put($cacheKey, $cacheEntry);
406
407 if ($cached && ($this->joinedAssociations === null || $this->joinedAssociations)) {
408 $this->storeJoinedAssociations($entity);
409 }
410
411 if ($cached) {
412 $this->cacheLogger?->entityCachePut($this->regionName, $cacheKey);
413 }
414
415 $this->cacheLogger?->entityCacheMiss($this->regionName, $cacheKey);
416
417 return $entity;
418 }
419
420 public function count(array|Criteria $criteria = []): int
421 {
422 return $this->persister->count($criteria);
423 }
424
425 /**
426 * {@inheritDoc}
427 */
428 public function loadCriteria(Criteria $criteria): array
429 {
430 $orderBy = $criteria->orderings();
431 $limit = $criteria->getMaxResults();
432 $offset = $criteria->getFirstResult();
433 $query = $this->persister->getSelectSQL($criteria);
434 $hash = $this->getHash($query, $criteria, $orderBy, $limit, $offset);
435 $rsm = $this->getResultSetMapping();
436 $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
437 $queryCache = $this->cache->getQueryCache($this->regionName);
438 $cacheResult = $queryCache->get($queryKey, $rsm);
439
440 if ($cacheResult !== null) {
441 $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
442
443 return $cacheResult;
444 }
445
446 $result = $this->persister->loadCriteria($criteria);
447 $cached = $queryCache->put($queryKey, $rsm, $result);
448
449 if ($result) {
450 $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
451 }
452
453 if ($cached) {
454 $this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
455 }
456
457 return $result;
458 }
459
460 /**
461 * {@inheritDoc}
462 */
463 public function loadManyToManyCollection(
464 AssociationMapping $assoc,
465 object $sourceEntity,
466 PersistentCollection $collection,
467 ): array {
468 $persister = $this->uow->getCollectionPersister($assoc);
469 $hasCache = ($persister instanceof CachedPersister);
470
471 if (! $hasCache) {
472 return $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
473 }
474
475 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
476 $key = $this->buildCollectionCacheKey($assoc, $ownerId);
477 $list = $persister->loadCollectionCache($collection, $key);
478
479 if ($list !== null) {
480 $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
481
482 return $list;
483 }
484
485 $list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
486
487 $persister->storeCollectionCache($key, $list);
488
489 $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
490
491 return $list;
492 }
493
494 public function loadOneToManyCollection(
495 AssociationMapping $assoc,
496 object $sourceEntity,
497 PersistentCollection $collection,
498 ): mixed {
499 $persister = $this->uow->getCollectionPersister($assoc);
500 $hasCache = ($persister instanceof CachedPersister);
501
502 if (! $hasCache) {
503 return $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
504 }
505
506 $ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
507 $key = $this->buildCollectionCacheKey($assoc, $ownerId);
508 $list = $persister->loadCollectionCache($collection, $key);
509
510 if ($list !== null) {
511 $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
512
513 return $list;
514 }
515
516 $list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
517
518 $persister->storeCollectionCache($key, $list);
519
520 $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
521
522 return $list;
523 }
524
525 /**
526 * {@inheritDoc}
527 */
528 public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null
529 {
530 return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier);
531 }
532
533 /**
534 * {@inheritDoc}
535 */
536 public function lock(array $criteria, LockMode|int $lockMode): void
537 {
538 $this->persister->lock($criteria, $lockMode);
539 }
540
541 /**
542 * {@inheritDoc}
543 */
544 public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void
545 {
546 $this->persister->refresh($id, $entity, $lockMode);
547 }
548
549 /** @param array<string, mixed> $ownerId */
550 protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId): CollectionCacheKey
551 {
552 $metadata = $this->metadataFactory->getMetadataFor($association->sourceEntity);
553 assert($metadata instanceof ClassMetadata);
554
555 return new CollectionCacheKey($metadata->rootEntityName, $association->fieldName, $ownerId);
556 }
557}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php
new file mode 100644
index 0000000..5fba56f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Entity;
6
7use Doctrine\ORM\Cache\EntityCacheKey;
8use Doctrine\ORM\Cache\EntityHydrator;
9use Doctrine\ORM\Cache\Persister\CachedPersister;
10use Doctrine\ORM\Persisters\Entity\EntityPersister;
11
12/**
13 * Interface for second level cache entity persisters.
14 */
15interface CachedEntityPersister extends CachedPersister, EntityPersister
16{
17 public function getEntityHydrator(): EntityHydrator;
18
19 public function storeEntityCache(object $entity, EntityCacheKey $key): bool;
20}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php
new file mode 100644
index 0000000..43c76ab
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php
@@ -0,0 +1,85 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Entity;
6
7use Doctrine\ORM\Cache\EntityCacheKey;
8
9/**
10 * Specific non-strict read/write cached entity persister
11 */
12class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister
13{
14 public function afterTransactionComplete(): void
15 {
16 $isChanged = false;
17
18 if (isset($this->queuedCache['insert'])) {
19 foreach ($this->queuedCache['insert'] as $entity) {
20 $isChanged = $this->updateCache($entity, $isChanged);
21 }
22 }
23
24 if (isset($this->queuedCache['update'])) {
25 foreach ($this->queuedCache['update'] as $entity) {
26 $isChanged = $this->updateCache($entity, $isChanged);
27 }
28 }
29
30 if (isset($this->queuedCache['delete'])) {
31 foreach ($this->queuedCache['delete'] as $key) {
32 $this->region->evict($key);
33
34 $isChanged = true;
35 }
36 }
37
38 if ($isChanged) {
39 $this->timestampRegion->update($this->timestampKey);
40 }
41
42 $this->queuedCache = [];
43 }
44
45 public function afterTransactionRolledBack(): void
46 {
47 $this->queuedCache = [];
48 }
49
50 public function delete(object $entity): bool
51 {
52 $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
53 $deleted = $this->persister->delete($entity);
54
55 if ($deleted) {
56 $this->region->evict($key);
57 }
58
59 $this->queuedCache['delete'][] = $key;
60
61 return $deleted;
62 }
63
64 public function update(object $entity): void
65 {
66 $this->persister->update($entity);
67
68 $this->queuedCache['update'][] = $entity;
69 }
70
71 private function updateCache(object $entity, bool $isChanged): bool
72 {
73 $class = $this->metadataFactory->getMetadataFor($entity::class);
74 $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity));
75 $entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
76 $cached = $this->region->put($key, $entry);
77 $isChanged = $isChanged || $cached;
78
79 if ($cached) {
80 $this->cacheLogger?->entityCachePut($this->regionName, $key);
81 }
82
83 return $isChanged;
84 }
85}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php
new file mode 100644
index 0000000..4cd1784
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Entity;
6
7use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
8use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
9
10/**
11 * Specific read-only region entity persister
12 */
13class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister
14{
15 public function update(object $entity): void
16 {
17 throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
18 }
19}
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php
new file mode 100644
index 0000000..a1ea0dc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php
@@ -0,0 +1,105 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Persister\Entity;
6
7use Doctrine\ORM\Cache\ConcurrentRegion;
8use Doctrine\ORM\Cache\EntityCacheKey;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Persisters\Entity\EntityPersister;
12
13/**
14 * Specific read-write entity persister
15 */
16class ReadWriteCachedEntityPersister extends AbstractEntityPersister
17{
18 public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class)
19 {
20 parent::__construct($persister, $region, $em, $class);
21 }
22
23 public function afterTransactionComplete(): void
24 {
25 $isChanged = true;
26
27 if (isset($this->queuedCache['update'])) {
28 foreach ($this->queuedCache['update'] as $item) {
29 $this->region->evict($item['key']);
30
31 $isChanged = true;
32 }
33 }
34
35 if (isset($this->queuedCache['delete'])) {
36 foreach ($this->queuedCache['delete'] as $item) {
37 $this->region->evict($item['key']);
38
39 $isChanged = true;
40 }
41 }
42
43 if ($isChanged) {
44 $this->timestampRegion->update($this->timestampKey);
45 }
46
47 $this->queuedCache = [];
48 }
49
50 public function afterTransactionRolledBack(): void
51 {
52 if (isset($this->queuedCache['update'])) {
53 foreach ($this->queuedCache['update'] as $item) {
54 $this->region->evict($item['key']);
55 }
56 }
57
58 if (isset($this->queuedCache['delete'])) {
59 foreach ($this->queuedCache['delete'] as $item) {
60 $this->region->evict($item['key']);
61 }
62 }
63
64 $this->queuedCache = [];
65 }
66
67 public function delete(object $entity): bool
68 {
69 $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
70 $lock = $this->region->lock($key);
71 $deleted = $this->persister->delete($entity);
72
73 if ($deleted) {
74 $this->region->evict($key);
75 }
76
77 if ($lock === null) {
78 return $deleted;
79 }
80
81 $this->queuedCache['delete'][] = [
82 'lock' => $lock,
83 'key' => $key,
84 ];
85
86 return $deleted;
87 }
88
89 public function update(object $entity): void
90 {
91 $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
92 $lock = $this->region->lock($key);
93
94 $this->persister->update($entity);
95
96 if ($lock === null) {
97 return;
98 }
99
100 $this->queuedCache['update'][] = [
101 'lock' => $lock,
102 'key' => $key,
103 ];
104 }
105}
diff --git a/vendor/doctrine/orm/src/Cache/QueryCache.php b/vendor/doctrine/orm/src/Cache/QueryCache.php
new file mode 100644
index 0000000..e697680
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/QueryCache.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Query\ResultSetMapping;
8
9/**
10 * Defines the contract for caches capable of storing query results.
11 * These caches should only concern themselves with storing the matching result ids.
12 */
13interface QueryCache
14{
15 public function clear(): bool;
16
17 /** @param mixed[] $hints */
18 public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool;
19
20 /**
21 * @param mixed[] $hints
22 *
23 * @return mixed[]|null
24 */
25 public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null;
26
27 public function getRegion(): Region;
28}
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php b/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php
new file mode 100644
index 0000000..1e39262
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function microtime;
8
9class QueryCacheEntry implements CacheEntry
10{
11 /**
12 * Time creation of this cache entry
13 */
14 public readonly float $time;
15
16 /** @param array<string, mixed> $result List of entity identifiers */
17 public function __construct(
18 public readonly array $result,
19 float|null $time = null,
20 ) {
21 $this->time = $time ?: microtime(true);
22 }
23
24 /** @param array<string, mixed> $values */
25 public static function __set_state(array $values): self
26 {
27 return new self($values['result'], $values['time']);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheKey.php b/vendor/doctrine/orm/src/Cache/QueryCacheKey.php
new file mode 100644
index 0000000..2372e5a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/QueryCacheKey.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache;
8
9/**
10 * A cache key that identifies a particular query.
11 */
12class QueryCacheKey extends CacheKey
13{
14 /** @param Cache::MODE_* $cacheMode */
15 public function __construct(
16 string $cacheId,
17 public readonly int $lifetime = 0,
18 public readonly int $cacheMode = Cache::MODE_NORMAL,
19 public readonly TimestampCacheKey|null $timestampKey = null,
20 ) {
21 parent::__construct($cacheId);
22 }
23}
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php b/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php
new file mode 100644
index 0000000..8a0d39f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Cache query validator interface.
9 */
10interface QueryCacheValidator
11{
12 /**
13 * Checks if the query entry is valid
14 */
15 public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool;
16}
diff --git a/vendor/doctrine/orm/src/Cache/Region.php b/vendor/doctrine/orm/src/Cache/Region.php
new file mode 100644
index 0000000..f7a1b26
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Region.php
@@ -0,0 +1,73 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use Doctrine\ORM\Cache\Exception\CacheException;
8
9/**
10 * Defines a contract for accessing a particular named region.
11 */
12interface Region
13{
14 /**
15 * Retrieve the name of this region.
16 */
17 public function getName(): string;
18
19 /**
20 * Determine whether this region contains data for the given key.
21 *
22 * @param CacheKey $key The cache key
23 */
24 public function contains(CacheKey $key): bool;
25
26 /**
27 * Get an item from the cache.
28 *
29 * @param CacheKey $key The key of the item to be retrieved.
30 *
31 * @return CacheEntry|null The cached entry or NULL
32 *
33 * @throws CacheException Indicates a problem accessing the item or region.
34 */
35 public function get(CacheKey $key): CacheEntry|null;
36
37 /**
38 * Get all items from the cache identified by $keys.
39 * It returns NULL if some elements can not be found.
40 *
41 * @param CollectionCacheEntry $collection The collection of the items to be retrieved.
42 *
43 * @return CacheEntry[]|null The cached entries or NULL if one or more entries can not be found
44 */
45 public function getMultiple(CollectionCacheEntry $collection): array|null;
46
47 /**
48 * Put an item into the cache.
49 *
50 * @param CacheKey $key The key under which to cache the item.
51 * @param CacheEntry $entry The entry to cache.
52 * @param Lock|null $lock The lock previously obtained.
53 *
54 * @throws CacheException Indicates a problem accessing the region.
55 */
56 public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool;
57
58 /**
59 * Remove an item from the cache.
60 *
61 * @param CacheKey $key The key under which to cache the item.
62 *
63 * @throws CacheException Indicates a problem accessing the region.
64 */
65 public function evict(CacheKey $key): bool;
66
67 /**
68 * Remove all contents of this particular cache region.
69 *
70 * @throws CacheException Indicates problem accessing the region.
71 */
72 public function evictAll(): bool;
73}
diff --git a/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php b/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php
new file mode 100644
index 0000000..0576195
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php
@@ -0,0 +1,113 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Region;
6
7use Doctrine\ORM\Cache\CacheEntry;
8use Doctrine\ORM\Cache\CacheKey;
9use Doctrine\ORM\Cache\CollectionCacheEntry;
10use Doctrine\ORM\Cache\Lock;
11use Doctrine\ORM\Cache\Region;
12use Psr\Cache\CacheItemInterface;
13use Psr\Cache\CacheItemPoolInterface;
14use Traversable;
15
16use function array_map;
17use function iterator_to_array;
18use function strtr;
19
20/**
21 * The simplest cache region compatible with all doctrine-cache drivers.
22 */
23class DefaultRegion implements Region
24{
25 private const REGION_KEY_SEPARATOR = '_';
26 private const REGION_PREFIX = 'DC2_REGION_';
27
28 public function __construct(
29 private readonly string $name,
30 private readonly CacheItemPoolInterface $cacheItemPool,
31 private readonly int $lifetime = 0,
32 ) {
33 }
34
35 public function getName(): string
36 {
37 return $this->name;
38 }
39
40 public function contains(CacheKey $key): bool
41 {
42 return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key));
43 }
44
45 public function get(CacheKey $key): CacheEntry|null
46 {
47 $item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key));
48 $entry = $item->isHit() ? $item->get() : null;
49
50 if (! $entry instanceof CacheEntry) {
51 return null;
52 }
53
54 return $entry;
55 }
56
57 public function getMultiple(CollectionCacheEntry $collection): array|null
58 {
59 $keys = array_map(
60 $this->getCacheEntryKey(...),
61 $collection->identifiers,
62 );
63 /** @var iterable<string, CacheItemInterface> $items */
64 $items = $this->cacheItemPool->getItems($keys);
65 if ($items instanceof Traversable) {
66 $items = iterator_to_array($items);
67 }
68
69 $result = [];
70 foreach ($keys as $arrayKey => $cacheKey) {
71 if (! isset($items[$cacheKey]) || ! $items[$cacheKey]->isHit()) {
72 return null;
73 }
74
75 $entry = $items[$cacheKey]->get();
76 if (! $entry instanceof CacheEntry) {
77 return null;
78 }
79
80 $result[$arrayKey] = $entry;
81 }
82
83 return $result;
84 }
85
86 public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool
87 {
88 $item = $this->cacheItemPool
89 ->getItem($this->getCacheEntryKey($key))
90 ->set($entry);
91
92 if ($this->lifetime > 0) {
93 $item->expiresAfter($this->lifetime);
94 }
95
96 return $this->cacheItemPool->save($item);
97 }
98
99 public function evict(CacheKey $key): bool
100 {
101 return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key));
102 }
103
104 public function evictAll(): bool
105 {
106 return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name);
107 }
108
109 private function getCacheEntryKey(CacheKey $key): string
110 {
111 return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________');
112 }
113}
diff --git a/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php b/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php
new file mode 100644
index 0000000..bedd6a6
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php
@@ -0,0 +1,194 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Region;
6
7use Doctrine\ORM\Cache\CacheEntry;
8use Doctrine\ORM\Cache\CacheKey;
9use Doctrine\ORM\Cache\CollectionCacheEntry;
10use Doctrine\ORM\Cache\ConcurrentRegion;
11use Doctrine\ORM\Cache\Lock;
12use Doctrine\ORM\Cache\Region;
13use InvalidArgumentException;
14
15use function array_filter;
16use function array_map;
17use function chmod;
18use function file_get_contents;
19use function file_put_contents;
20use function fileatime;
21use function glob;
22use function is_dir;
23use function is_file;
24use function is_writable;
25use function mkdir;
26use function sprintf;
27use function time;
28use function unlink;
29
30use const DIRECTORY_SEPARATOR;
31use const LOCK_EX;
32
33/**
34 * Very naive concurrent region, based on file locks.
35 */
36class FileLockRegion implements ConcurrentRegion
37{
38 final public const LOCK_EXTENSION = 'lock';
39
40 /**
41 * @param numeric-string|int $lockLifetime
42 *
43 * @throws InvalidArgumentException
44 */
45 public function __construct(
46 private readonly Region $region,
47 private readonly string $directory,
48 private readonly string|int $lockLifetime,
49 ) {
50 if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) {
51 throw new InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory));
52 }
53
54 if (! is_writable($directory)) {
55 throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory));
56 }
57 }
58
59 private function isLocked(CacheKey $key, Lock|null $lock = null): bool
60 {
61 $filename = $this->getLockFileName($key);
62
63 if (! is_file($filename)) {
64 return false;
65 }
66
67 $time = $this->getLockTime($filename);
68 $content = $this->getLockContent($filename);
69
70 if ($content === false || $time === false) {
71 @unlink($filename);
72
73 return false;
74 }
75
76 if ($lock && $content === $lock->value) {
77 return false;
78 }
79
80 // outdated lock
81 if ($time + $this->lockLifetime <= time()) {
82 @unlink($filename);
83
84 return false;
85 }
86
87 return true;
88 }
89
90 private function getLockFileName(CacheKey $key): string
91 {
92 return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION;
93 }
94
95 private function getLockContent(string $filename): string|false
96 {
97 return @file_get_contents($filename);
98 }
99
100 private function getLockTime(string $filename): int|false
101 {
102 return @fileatime($filename);
103 }
104
105 public function getName(): string
106 {
107 return $this->region->getName();
108 }
109
110 public function contains(CacheKey $key): bool
111 {
112 if ($this->isLocked($key)) {
113 return false;
114 }
115
116 return $this->region->contains($key);
117 }
118
119 public function get(CacheKey $key): CacheEntry|null
120 {
121 if ($this->isLocked($key)) {
122 return null;
123 }
124
125 return $this->region->get($key);
126 }
127
128 public function getMultiple(CollectionCacheEntry $collection): array|null
129 {
130 if (array_filter(array_map($this->isLocked(...), $collection->identifiers))) {
131 return null;
132 }
133
134 return $this->region->getMultiple($collection);
135 }
136
137 public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool
138 {
139 if ($this->isLocked($key, $lock)) {
140 return false;
141 }
142
143 return $this->region->put($key, $entry);
144 }
145
146 public function evict(CacheKey $key): bool
147 {
148 if ($this->isLocked($key)) {
149 @unlink($this->getLockFileName($key));
150 }
151
152 return $this->region->evict($key);
153 }
154
155 public function evictAll(): bool
156 {
157 // The check below is necessary because on some platforms glob returns false
158 // when nothing matched (even though no errors occurred)
159 $filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: [];
160
161 foreach ($filenames as $filename) {
162 @unlink($filename);
163 }
164
165 return $this->region->evictAll();
166 }
167
168 public function lock(CacheKey $key): Lock|null
169 {
170 if ($this->isLocked($key)) {
171 return null;
172 }
173
174 $lock = Lock::createLockRead();
175 $filename = $this->getLockFileName($key);
176
177 if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) {
178 return null;
179 }
180
181 chmod($filename, 0664);
182
183 return $lock;
184 }
185
186 public function unlock(CacheKey $key, Lock $lock): bool
187 {
188 if ($this->isLocked($key, $lock)) {
189 return false;
190 }
191
192 return @unlink($this->getLockFileName($key));
193 }
194}
diff --git a/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php b/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php
new file mode 100644
index 0000000..aa75a90
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache\Region;
6
7use Doctrine\ORM\Cache\CacheKey;
8use Doctrine\ORM\Cache\TimestampCacheEntry;
9use Doctrine\ORM\Cache\TimestampRegion;
10
11/**
12 * Tracks the timestamps of the most recent updates to particular keys.
13 */
14class UpdateTimestampCache extends DefaultRegion implements TimestampRegion
15{
16 public function update(CacheKey $key): void
17 {
18 $this->put($key, new TimestampCacheEntry());
19 }
20}
diff --git a/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php b/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php
new file mode 100644
index 0000000..a852831
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php
@@ -0,0 +1,63 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Cache regions configuration
9 */
10class RegionsConfiguration
11{
12 /** @var array<string,int> */
13 private array $lifetimes = [];
14
15 /** @var array<string,int> */
16 private array $lockLifetimes = [];
17
18 public function __construct(
19 private int $defaultLifetime = 3600,
20 private int $defaultLockLifetime = 60,
21 ) {
22 }
23
24 public function getDefaultLifetime(): int
25 {
26 return $this->defaultLifetime;
27 }
28
29 public function setDefaultLifetime(int $defaultLifetime): void
30 {
31 $this->defaultLifetime = $defaultLifetime;
32 }
33
34 public function getDefaultLockLifetime(): int
35 {
36 return $this->defaultLockLifetime;
37 }
38
39 public function setDefaultLockLifetime(int $defaultLockLifetime): void
40 {
41 $this->defaultLockLifetime = $defaultLockLifetime;
42 }
43
44 public function getLifetime(string $regionName): int
45 {
46 return $this->lifetimes[$regionName] ?? $this->defaultLifetime;
47 }
48
49 public function setLifetime(string $name, int $lifetime): void
50 {
51 $this->lifetimes[$name] = $lifetime;
52 }
53
54 public function getLockLifetime(string $regionName): int
55 {
56 return $this->lockLifetimes[$regionName] ?? $this->defaultLockLifetime;
57 }
58
59 public function setLockLifetime(string $name, int $lifetime): void
60 {
61 $this->lockLifetimes[$name] = $lifetime;
62 }
63}
diff --git a/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php b/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php
new file mode 100644
index 0000000..60c9175
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function microtime;
8
9class TimestampCacheEntry implements CacheEntry
10{
11 public readonly float $time;
12
13 public function __construct(float|null $time = null)
14 {
15 $this->time = $time ?? microtime(true);
16 }
17
18 /**
19 * Creates a new TimestampCacheEntry
20 *
21 * This method allow Doctrine\Common\Cache\PhpFileCache compatibility
22 *
23 * @param array<string,float> $values array containing property values
24 */
25 public static function __set_state(array $values): TimestampCacheEntry
26 {
27 return new self($values['time']);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php b/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php
new file mode 100644
index 0000000..5aef4c5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * A key that identifies a timestamped space.
9 */
10class TimestampCacheKey extends CacheKey
11{
12 /** @param string $space Result cache id */
13 public function __construct(string $space)
14 {
15 parent::__construct($space);
16 }
17}
diff --git a/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php b/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php
new file mode 100644
index 0000000..9824088
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php
@@ -0,0 +1,38 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7use function microtime;
8
9class TimestampQueryCacheValidator implements QueryCacheValidator
10{
11 public function __construct(private readonly TimestampRegion $timestampRegion)
12 {
13 }
14
15 public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool
16 {
17 if ($this->regionUpdated($key, $entry)) {
18 return false;
19 }
20
21 if ($key->lifetime === 0) {
22 return true;
23 }
24
25 return $entry->time + $key->lifetime > microtime(true);
26 }
27
28 private function regionUpdated(QueryCacheKey $key, QueryCacheEntry $entry): bool
29 {
30 if ($key->timestampKey === null) {
31 return false;
32 }
33
34 $timestamp = $this->timestampRegion->get($key->timestampKey);
35
36 return $timestamp && $timestamp->time > $entry->time;
37 }
38}
diff --git a/vendor/doctrine/orm/src/Cache/TimestampRegion.php b/vendor/doctrine/orm/src/Cache/TimestampRegion.php
new file mode 100644
index 0000000..b74fa8d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Cache/TimestampRegion.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Cache;
6
7/**
8 * Defines the contract for a cache region which will specifically be used to store entity "update timestamps".
9 */
10interface TimestampRegion extends Region
11{
12 /**
13 * Update a specific key into the cache region.
14 *
15 * @throws LockException Indicates a problem accessing the region.
16 */
17 public function update(CacheKey $key): void;
18}
diff --git a/vendor/doctrine/orm/src/Configuration.php b/vendor/doctrine/orm/src/Configuration.php
new file mode 100644
index 0000000..b30764e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Configuration.php
@@ -0,0 +1,649 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\ORM\Cache\CacheConfiguration;
9use Doctrine\ORM\Exception\InvalidEntityRepository;
10use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
11use Doctrine\ORM\Mapping\ClassMetadata;
12use Doctrine\ORM\Mapping\ClassMetadataFactory;
13use Doctrine\ORM\Mapping\DefaultEntityListenerResolver;
14use Doctrine\ORM\Mapping\DefaultNamingStrategy;
15use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
16use Doctrine\ORM\Mapping\EntityListenerResolver;
17use Doctrine\ORM\Mapping\NamingStrategy;
18use Doctrine\ORM\Mapping\QuoteStrategy;
19use Doctrine\ORM\Mapping\TypedFieldMapper;
20use Doctrine\ORM\Proxy\ProxyFactory;
21use Doctrine\ORM\Query\AST\Functions\FunctionNode;
22use Doctrine\ORM\Query\Filter\SQLFilter;
23use Doctrine\ORM\Repository\DefaultRepositoryFactory;
24use Doctrine\ORM\Repository\RepositoryFactory;
25use Doctrine\Persistence\Mapping\Driver\MappingDriver;
26use LogicException;
27use Psr\Cache\CacheItemPoolInterface;
28
29use function class_exists;
30use function is_a;
31use function strtolower;
32
33/**
34 * Configuration container for all configuration options of Doctrine.
35 * It combines all configuration options from DBAL & ORM.
36 *
37 * Internal note: When adding a new configuration option just write a getter/setter pair.
38 */
39class Configuration extends \Doctrine\DBAL\Configuration
40{
41 /** @var mixed[] */
42 protected array $attributes = [];
43
44 /** @psalm-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */
45 private $identityGenerationPreferences = [];
46
47 /** @psalm-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
48 public function setIdentityGenerationPreferences(array $value): void
49 {
50 $this->identityGenerationPreferences = $value;
51 }
52
53 /** @psalm-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
54 public function getIdentityGenerationPreferences(): array
55 {
56 return $this->identityGenerationPreferences;
57 }
58
59 /**
60 * Sets the directory where Doctrine generates any necessary proxy class files.
61 */
62 public function setProxyDir(string $dir): void
63 {
64 $this->attributes['proxyDir'] = $dir;
65 }
66
67 /**
68 * Gets the directory where Doctrine generates any necessary proxy class files.
69 */
70 public function getProxyDir(): string|null
71 {
72 return $this->attributes['proxyDir'] ?? null;
73 }
74
75 /**
76 * Gets the strategy for automatically generating proxy classes.
77 *
78 * @return ProxyFactory::AUTOGENERATE_*
79 */
80 public function getAutoGenerateProxyClasses(): int
81 {
82 return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS;
83 }
84
85 /**
86 * Sets the strategy for automatically generating proxy classes.
87 *
88 * @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
89 */
90 public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void
91 {
92 $this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate;
93 }
94
95 /**
96 * Gets the namespace where proxy classes reside.
97 */
98 public function getProxyNamespace(): string|null
99 {
100 return $this->attributes['proxyNamespace'] ?? null;
101 }
102
103 /**
104 * Sets the namespace where proxy classes reside.
105 */
106 public function setProxyNamespace(string $ns): void
107 {
108 $this->attributes['proxyNamespace'] = $ns;
109 }
110
111 /**
112 * Sets the cache driver implementation that is used for metadata caching.
113 *
114 * @todo Force parameter to be a Closure to ensure lazy evaluation
115 * (as soon as a metadata cache is in effect, the driver never needs to initialize).
116 */
117 public function setMetadataDriverImpl(MappingDriver $driverImpl): void
118 {
119 $this->attributes['metadataDriverImpl'] = $driverImpl;
120 }
121
122 /**
123 * Sets the entity alias map.
124 *
125 * @psalm-param array<string, string> $entityNamespaces
126 */
127 public function setEntityNamespaces(array $entityNamespaces): void
128 {
129 $this->attributes['entityNamespaces'] = $entityNamespaces;
130 }
131
132 /**
133 * Retrieves the list of registered entity namespace aliases.
134 *
135 * @psalm-return array<string, string>
136 */
137 public function getEntityNamespaces(): array
138 {
139 return $this->attributes['entityNamespaces'];
140 }
141
142 /**
143 * Gets the cache driver implementation that is used for the mapping metadata.
144 */
145 public function getMetadataDriverImpl(): MappingDriver|null
146 {
147 return $this->attributes['metadataDriverImpl'] ?? null;
148 }
149
150 /**
151 * Gets the cache driver implementation that is used for the query cache (SQL cache).
152 */
153 public function getQueryCache(): CacheItemPoolInterface|null
154 {
155 return $this->attributes['queryCache'] ?? null;
156 }
157
158 /**
159 * Sets the cache driver implementation that is used for the query cache (SQL cache).
160 */
161 public function setQueryCache(CacheItemPoolInterface $cache): void
162 {
163 $this->attributes['queryCache'] = $cache;
164 }
165
166 public function getHydrationCache(): CacheItemPoolInterface|null
167 {
168 return $this->attributes['hydrationCache'] ?? null;
169 }
170
171 public function setHydrationCache(CacheItemPoolInterface $cache): void
172 {
173 $this->attributes['hydrationCache'] = $cache;
174 }
175
176 public function getMetadataCache(): CacheItemPoolInterface|null
177 {
178 return $this->attributes['metadataCache'] ?? null;
179 }
180
181 public function setMetadataCache(CacheItemPoolInterface $cache): void
182 {
183 $this->attributes['metadataCache'] = $cache;
184 }
185
186 /**
187 * Registers a custom DQL function that produces a string value.
188 * Such a function can then be used in any DQL statement in any place where string
189 * functions are allowed.
190 *
191 * DQL function names are case-insensitive.
192 *
193 * @param class-string|callable $className Class name or a callable that returns the function.
194 * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className
195 */
196 public function addCustomStringFunction(string $name, string|callable $className): void
197 {
198 $this->attributes['customStringFunctions'][strtolower($name)] = $className;
199 }
200
201 /**
202 * Gets the implementation class name of a registered custom string DQL function.
203 *
204 * @psalm-return class-string<FunctionNode>|callable(string):FunctionNode|null
205 */
206 public function getCustomStringFunction(string $name): string|callable|null
207 {
208 $name = strtolower($name);
209
210 return $this->attributes['customStringFunctions'][$name] ?? null;
211 }
212
213 /**
214 * Sets a map of custom DQL string functions.
215 *
216 * Keys must be function names and values the FQCN of the implementing class.
217 * The function names will be case-insensitive in DQL.
218 *
219 * Any previously added string functions are discarded.
220 *
221 * @psalm-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions The map of custom
222 * DQL string functions.
223 */
224 public function setCustomStringFunctions(array $functions): void
225 {
226 foreach ($functions as $name => $className) {
227 $this->addCustomStringFunction($name, $className);
228 }
229 }
230
231 /**
232 * Registers a custom DQL function that produces a numeric value.
233 * Such a function can then be used in any DQL statement in any place where numeric
234 * functions are allowed.
235 *
236 * DQL function names are case-insensitive.
237 *
238 * @param class-string|callable $className Class name or a callable that returns the function.
239 * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className
240 */
241 public function addCustomNumericFunction(string $name, string|callable $className): void
242 {
243 $this->attributes['customNumericFunctions'][strtolower($name)] = $className;
244 }
245
246 /**
247 * Gets the implementation class name of a registered custom numeric DQL function.
248 *
249 * @psalm-return ?class-string<FunctionNode>|callable(string):FunctionNode
250 */
251 public function getCustomNumericFunction(string $name): string|callable|null
252 {
253 $name = strtolower($name);
254
255 return $this->attributes['customNumericFunctions'][$name] ?? null;
256 }
257
258 /**
259 * Sets a map of custom DQL numeric functions.
260 *
261 * Keys must be function names and values the FQCN of the implementing class.
262 * The function names will be case-insensitive in DQL.
263 *
264 * Any previously added numeric functions are discarded.
265 *
266 * @psalm-param array<string, class-string> $functions The map of custom
267 * DQL numeric functions.
268 */
269 public function setCustomNumericFunctions(array $functions): void
270 {
271 foreach ($functions as $name => $className) {
272 $this->addCustomNumericFunction($name, $className);
273 }
274 }
275
276 /**
277 * Registers a custom DQL function that produces a date/time value.
278 * Such a function can then be used in any DQL statement in any place where date/time
279 * functions are allowed.
280 *
281 * DQL function names are case-insensitive.
282 *
283 * @param string|callable $className Class name or a callable that returns the function.
284 * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className
285 */
286 public function addCustomDatetimeFunction(string $name, string|callable $className): void
287 {
288 $this->attributes['customDatetimeFunctions'][strtolower($name)] = $className;
289 }
290
291 /**
292 * Gets the implementation class name of a registered custom date/time DQL function.
293 *
294 * @psalm-return class-string|callable|null
295 */
296 public function getCustomDatetimeFunction(string $name): string|callable|null
297 {
298 $name = strtolower($name);
299
300 return $this->attributes['customDatetimeFunctions'][$name] ?? null;
301 }
302
303 /**
304 * Sets a map of custom DQL date/time functions.
305 *
306 * Keys must be function names and values the FQCN of the implementing class.
307 * The function names will be case-insensitive in DQL.
308 *
309 * Any previously added date/time functions are discarded.
310 *
311 * @param array $functions The map of custom DQL date/time functions.
312 * @psalm-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions
313 */
314 public function setCustomDatetimeFunctions(array $functions): void
315 {
316 foreach ($functions as $name => $className) {
317 $this->addCustomDatetimeFunction($name, $className);
318 }
319 }
320
321 /**
322 * Sets a TypedFieldMapper for php typed fields to DBAL types auto-completion.
323 */
324 public function setTypedFieldMapper(TypedFieldMapper|null $typedFieldMapper): void
325 {
326 $this->attributes['typedFieldMapper'] = $typedFieldMapper;
327 }
328
329 /**
330 * Gets a TypedFieldMapper for php typed fields to DBAL types auto-completion.
331 */
332 public function getTypedFieldMapper(): TypedFieldMapper|null
333 {
334 return $this->attributes['typedFieldMapper'] ?? null;
335 }
336
337 /**
338 * Sets the custom hydrator modes in one pass.
339 *
340 * @param array<string, class-string<AbstractHydrator>> $modes An array of ($modeName => $hydrator).
341 */
342 public function setCustomHydrationModes(array $modes): void
343 {
344 $this->attributes['customHydrationModes'] = [];
345
346 foreach ($modes as $modeName => $hydrator) {
347 $this->addCustomHydrationMode($modeName, $hydrator);
348 }
349 }
350
351 /**
352 * Gets the hydrator class for the given hydration mode name.
353 *
354 * @psalm-return class-string<AbstractHydrator>|null
355 */
356 public function getCustomHydrationMode(string $modeName): string|null
357 {
358 return $this->attributes['customHydrationModes'][$modeName] ?? null;
359 }
360
361 /**
362 * Adds a custom hydration mode.
363 *
364 * @psalm-param class-string<AbstractHydrator> $hydrator
365 */
366 public function addCustomHydrationMode(string $modeName, string $hydrator): void
367 {
368 $this->attributes['customHydrationModes'][$modeName] = $hydrator;
369 }
370
371 /**
372 * Sets a class metadata factory.
373 *
374 * @psalm-param class-string $cmfName
375 */
376 public function setClassMetadataFactoryName(string $cmfName): void
377 {
378 $this->attributes['classMetadataFactoryName'] = $cmfName;
379 }
380
381 /** @psalm-return class-string */
382 public function getClassMetadataFactoryName(): string
383 {
384 if (! isset($this->attributes['classMetadataFactoryName'])) {
385 $this->attributes['classMetadataFactoryName'] = ClassMetadataFactory::class;
386 }
387
388 return $this->attributes['classMetadataFactoryName'];
389 }
390
391 /**
392 * Adds a filter to the list of possible filters.
393 *
394 * @param string $className The class name of the filter.
395 * @psalm-param class-string<SQLFilter> $className
396 */
397 public function addFilter(string $name, string $className): void
398 {
399 $this->attributes['filters'][$name] = $className;
400 }
401
402 /**
403 * Gets the class name for a given filter name.
404 *
405 * @return string|null The class name of the filter, or null if it is not
406 * defined.
407 * @psalm-return class-string<SQLFilter>|null
408 */
409 public function getFilterClassName(string $name): string|null
410 {
411 return $this->attributes['filters'][$name] ?? null;
412 }
413
414 /**
415 * Sets default repository class.
416 *
417 * @psalm-param class-string<EntityRepository> $className
418 *
419 * @throws InvalidEntityRepository If $classname is not an ObjectRepository.
420 */
421 public function setDefaultRepositoryClassName(string $className): void
422 {
423 if (! class_exists($className) || ! is_a($className, EntityRepository::class, true)) {
424 throw InvalidEntityRepository::fromClassName($className);
425 }
426
427 $this->attributes['defaultRepositoryClassName'] = $className;
428 }
429
430 /**
431 * Get default repository class.
432 *
433 * @psalm-return class-string<EntityRepository>
434 */
435 public function getDefaultRepositoryClassName(): string
436 {
437 return $this->attributes['defaultRepositoryClassName'] ?? EntityRepository::class;
438 }
439
440 /**
441 * Sets naming strategy.
442 */
443 public function setNamingStrategy(NamingStrategy $namingStrategy): void
444 {
445 $this->attributes['namingStrategy'] = $namingStrategy;
446 }
447
448 /**
449 * Gets naming strategy..
450 */
451 public function getNamingStrategy(): NamingStrategy
452 {
453 if (! isset($this->attributes['namingStrategy'])) {
454 $this->attributes['namingStrategy'] = new DefaultNamingStrategy();
455 }
456
457 return $this->attributes['namingStrategy'];
458 }
459
460 /**
461 * Sets quote strategy.
462 */
463 public function setQuoteStrategy(QuoteStrategy $quoteStrategy): void
464 {
465 $this->attributes['quoteStrategy'] = $quoteStrategy;
466 }
467
468 /**
469 * Gets quote strategy.
470 */
471 public function getQuoteStrategy(): QuoteStrategy
472 {
473 if (! isset($this->attributes['quoteStrategy'])) {
474 $this->attributes['quoteStrategy'] = new DefaultQuoteStrategy();
475 }
476
477 return $this->attributes['quoteStrategy'];
478 }
479
480 /**
481 * Set the entity listener resolver.
482 */
483 public function setEntityListenerResolver(EntityListenerResolver $resolver): void
484 {
485 $this->attributes['entityListenerResolver'] = $resolver;
486 }
487
488 /**
489 * Get the entity listener resolver.
490 */
491 public function getEntityListenerResolver(): EntityListenerResolver
492 {
493 if (! isset($this->attributes['entityListenerResolver'])) {
494 $this->attributes['entityListenerResolver'] = new DefaultEntityListenerResolver();
495 }
496
497 return $this->attributes['entityListenerResolver'];
498 }
499
500 /**
501 * Set the entity repository factory.
502 */
503 public function setRepositoryFactory(RepositoryFactory $repositoryFactory): void
504 {
505 $this->attributes['repositoryFactory'] = $repositoryFactory;
506 }
507
508 /**
509 * Get the entity repository factory.
510 */
511 public function getRepositoryFactory(): RepositoryFactory
512 {
513 return $this->attributes['repositoryFactory'] ?? new DefaultRepositoryFactory();
514 }
515
516 public function isSecondLevelCacheEnabled(): bool
517 {
518 return $this->attributes['isSecondLevelCacheEnabled'] ?? false;
519 }
520
521 public function setSecondLevelCacheEnabled(bool $flag = true): void
522 {
523 $this->attributes['isSecondLevelCacheEnabled'] = $flag;
524 }
525
526 public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig): void
527 {
528 $this->attributes['secondLevelCacheConfiguration'] = $cacheConfig;
529 }
530
531 public function getSecondLevelCacheConfiguration(): CacheConfiguration|null
532 {
533 if (! isset($this->attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) {
534 $this->attributes['secondLevelCacheConfiguration'] = new CacheConfiguration();
535 }
536
537 return $this->attributes['secondLevelCacheConfiguration'] ?? null;
538 }
539
540 /**
541 * Returns query hints, which will be applied to every query in application
542 *
543 * @psalm-return array<string, mixed>
544 */
545 public function getDefaultQueryHints(): array
546 {
547 return $this->attributes['defaultQueryHints'] ?? [];
548 }
549
550 /**
551 * Sets array of query hints, which will be applied to every query in application
552 *
553 * @psalm-param array<string, mixed> $defaultQueryHints
554 */
555 public function setDefaultQueryHints(array $defaultQueryHints): void
556 {
557 $this->attributes['defaultQueryHints'] = $defaultQueryHints;
558 }
559
560 /**
561 * Gets the value of a default query hint. If the hint name is not recognized, FALSE is returned.
562 *
563 * @return mixed The value of the hint or FALSE, if the hint name is not recognized.
564 */
565 public function getDefaultQueryHint(string $name): mixed
566 {
567 return $this->attributes['defaultQueryHints'][$name] ?? false;
568 }
569
570 /**
571 * Sets a default query hint. If the hint name is not recognized, it is silently ignored.
572 */
573 public function setDefaultQueryHint(string $name, mixed $value): void
574 {
575 $this->attributes['defaultQueryHints'][$name] = $value;
576 }
577
578 /**
579 * Gets a list of entity class names to be ignored by the SchemaTool
580 *
581 * @return list<class-string>
582 */
583 public function getSchemaIgnoreClasses(): array
584 {
585 return $this->attributes['schemaIgnoreClasses'] ?? [];
586 }
587
588 /**
589 * Sets a list of entity class names to be ignored by the SchemaTool
590 *
591 * @param list<class-string> $schemaIgnoreClasses List of entity class names
592 */
593 public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void
594 {
595 $this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses;
596 }
597
598 /**
599 * To be deprecated in 3.1.0
600 *
601 * @return true
602 */
603 public function isLazyGhostObjectEnabled(): bool
604 {
605 return true;
606 }
607
608 /** To be deprecated in 3.1.0 */
609 public function setLazyGhostObjectEnabled(bool $flag): void
610 {
611 if (! $flag) {
612 throw new LogicException(<<<'EXCEPTION'
613 The lazy ghost object feature cannot be disabled anymore.
614 Please remove the call to setLazyGhostObjectEnabled(false).
615 EXCEPTION);
616 }
617 }
618
619 /** To be deprecated in 3.1.0 */
620 public function setRejectIdCollisionInIdentityMap(bool $flag): void
621 {
622 if (! $flag) {
623 throw new LogicException(<<<'EXCEPTION'
624 Rejecting ID collisions in the identity map cannot be disabled anymore.
625 Please remove the call to setRejectIdCollisionInIdentityMap(false).
626 EXCEPTION);
627 }
628 }
629
630 /**
631 * To be deprecated in 3.1.0
632 *
633 * @return true
634 */
635 public function isRejectIdCollisionInIdentityMapEnabled(): bool
636 {
637 return true;
638 }
639
640 public function setEagerFetchBatchSize(int $batchSize = 100): void
641 {
642 $this->attributes['fetchModeSubselectBatchSize'] = $batchSize;
643 }
644
645 public function getEagerFetchBatchSize(): int
646 {
647 return $this->attributes['fetchModeSubselectBatchSize'] ?? 100;
648 }
649}
diff --git a/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php b/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php
new file mode 100644
index 0000000..6f1b041
--- /dev/null
+++ b/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php
@@ -0,0 +1,174 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Decorator;
6
7use DateTimeInterface;
8use Doctrine\Common\EventManager;
9use Doctrine\DBAL\Connection;
10use Doctrine\DBAL\LockMode;
11use Doctrine\ORM\Cache;
12use Doctrine\ORM\Configuration;
13use Doctrine\ORM\EntityManagerInterface;
14use Doctrine\ORM\EntityRepository;
15use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
16use Doctrine\ORM\Mapping\ClassMetadata;
17use Doctrine\ORM\Mapping\ClassMetadataFactory;
18use Doctrine\ORM\NativeQuery;
19use Doctrine\ORM\Proxy\ProxyFactory;
20use Doctrine\ORM\Query;
21use Doctrine\ORM\Query\Expr;
22use Doctrine\ORM\Query\FilterCollection;
23use Doctrine\ORM\Query\ResultSetMapping;
24use Doctrine\ORM\QueryBuilder;
25use Doctrine\ORM\UnitOfWork;
26use Doctrine\Persistence\ObjectManagerDecorator;
27
28/**
29 * Base class for EntityManager decorators
30 *
31 * @extends ObjectManagerDecorator<EntityManagerInterface>
32 */
33abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
34{
35 public function __construct(EntityManagerInterface $wrapped)
36 {
37 $this->wrapped = $wrapped;
38 }
39
40 public function getRepository(string $className): EntityRepository
41 {
42 return $this->wrapped->getRepository($className);
43 }
44
45 public function getMetadataFactory(): ClassMetadataFactory
46 {
47 return $this->wrapped->getMetadataFactory();
48 }
49
50 public function getClassMetadata(string $className): ClassMetadata
51 {
52 return $this->wrapped->getClassMetadata($className);
53 }
54
55 public function getConnection(): Connection
56 {
57 return $this->wrapped->getConnection();
58 }
59
60 public function getExpressionBuilder(): Expr
61 {
62 return $this->wrapped->getExpressionBuilder();
63 }
64
65 public function beginTransaction(): void
66 {
67 $this->wrapped->beginTransaction();
68 }
69
70 public function wrapInTransaction(callable $func): mixed
71 {
72 return $this->wrapped->wrapInTransaction($func);
73 }
74
75 public function commit(): void
76 {
77 $this->wrapped->commit();
78 }
79
80 public function rollback(): void
81 {
82 $this->wrapped->rollback();
83 }
84
85 public function createQuery(string $dql = ''): Query
86 {
87 return $this->wrapped->createQuery($dql);
88 }
89
90 public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery
91 {
92 return $this->wrapped->createNativeQuery($sql, $rsm);
93 }
94
95 public function createQueryBuilder(): QueryBuilder
96 {
97 return $this->wrapped->createQueryBuilder();
98 }
99
100 public function getReference(string $entityName, mixed $id): object|null
101 {
102 return $this->wrapped->getReference($entityName, $id);
103 }
104
105 public function close(): void
106 {
107 $this->wrapped->close();
108 }
109
110 public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void
111 {
112 $this->wrapped->lock($entity, $lockMode, $lockVersion);
113 }
114
115 public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
116 {
117 return $this->wrapped->find($className, $id, $lockMode, $lockVersion);
118 }
119
120 public function refresh(object $object, LockMode|int|null $lockMode = null): void
121 {
122 $this->wrapped->refresh($object, $lockMode);
123 }
124
125 public function getEventManager(): EventManager
126 {
127 return $this->wrapped->getEventManager();
128 }
129
130 public function getConfiguration(): Configuration
131 {
132 return $this->wrapped->getConfiguration();
133 }
134
135 public function isOpen(): bool
136 {
137 return $this->wrapped->isOpen();
138 }
139
140 public function getUnitOfWork(): UnitOfWork
141 {
142 return $this->wrapped->getUnitOfWork();
143 }
144
145 public function newHydrator(string|int $hydrationMode): AbstractHydrator
146 {
147 return $this->wrapped->newHydrator($hydrationMode);
148 }
149
150 public function getProxyFactory(): ProxyFactory
151 {
152 return $this->wrapped->getProxyFactory();
153 }
154
155 public function getFilters(): FilterCollection
156 {
157 return $this->wrapped->getFilters();
158 }
159
160 public function isFiltersStateClean(): bool
161 {
162 return $this->wrapped->isFiltersStateClean();
163 }
164
165 public function hasFilters(): bool
166 {
167 return $this->wrapped->hasFilters();
168 }
169
170 public function getCache(): Cache|null
171 {
172 return $this->wrapped->getCache();
173 }
174}
diff --git a/vendor/doctrine/orm/src/EntityManager.php b/vendor/doctrine/orm/src/EntityManager.php
new file mode 100644
index 0000000..8045ac2
--- /dev/null
+++ b/vendor/doctrine/orm/src/EntityManager.php
@@ -0,0 +1,626 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use BackedEnum;
8use DateTimeInterface;
9use Doctrine\Common\EventManager;
10use Doctrine\DBAL\Connection;
11use Doctrine\DBAL\LockMode;
12use Doctrine\ORM\Exception\EntityManagerClosed;
13use Doctrine\ORM\Exception\InvalidHydrationMode;
14use Doctrine\ORM\Exception\MissingIdentifierField;
15use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
16use Doctrine\ORM\Exception\ORMException;
17use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
18use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
19use Doctrine\ORM\Mapping\ClassMetadata;
20use Doctrine\ORM\Mapping\ClassMetadataFactory;
21use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
22use Doctrine\ORM\Proxy\ProxyFactory;
23use Doctrine\ORM\Query\Expr;
24use Doctrine\ORM\Query\FilterCollection;
25use Doctrine\ORM\Query\ResultSetMapping;
26use Doctrine\ORM\Repository\RepositoryFactory;
27use Throwable;
28
29use function array_keys;
30use function is_array;
31use function is_object;
32use function ltrim;
33use function method_exists;
34
35/**
36 * The EntityManager is the central access point to ORM functionality.
37 *
38 * It is a facade to all different ORM subsystems such as UnitOfWork,
39 * Query Language and Repository API. The quickest way to obtain a fully
40 * configured EntityManager is:
41 *
42 * use Doctrine\ORM\Tools\ORMSetup;
43 * use Doctrine\ORM\EntityManager;
44 *
45 * $paths = ['/path/to/entity/mapping/files'];
46 *
47 * $config = ORMSetup::createAttributeMetadataConfiguration($paths);
48 * $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
49 * $entityManager = new EntityManager($connection, $config);
50 *
51 * For more information see
52 * {@link http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/configuration.html}
53 *
54 * You should never attempt to inherit from the EntityManager: Inheritance
55 * is not a valid extension point for the EntityManager. Instead you
56 * should take a look at the {@see \Doctrine\ORM\Decorator\EntityManagerDecorator}
57 * and wrap your entity manager in a decorator.
58 *
59 * @final
60 */
61class EntityManager implements EntityManagerInterface
62{
63 /**
64 * The metadata factory, used to retrieve the ORM metadata of entity classes.
65 */
66 private ClassMetadataFactory $metadataFactory;
67
68 /**
69 * The UnitOfWork used to coordinate object-level transactions.
70 */
71 private UnitOfWork $unitOfWork;
72
73 /**
74 * The event manager that is the central point of the event system.
75 */
76 private EventManager $eventManager;
77
78 /**
79 * The proxy factory used to create dynamic proxies.
80 */
81 private ProxyFactory $proxyFactory;
82
83 /**
84 * The repository factory used to create dynamic repositories.
85 */
86 private RepositoryFactory $repositoryFactory;
87
88 /**
89 * The expression builder instance used to generate query expressions.
90 */
91 private Expr|null $expressionBuilder = null;
92
93 /**
94 * Whether the EntityManager is closed or not.
95 */
96 private bool $closed = false;
97
98 /**
99 * Collection of query filters.
100 */
101 private FilterCollection|null $filterCollection = null;
102
103 /**
104 * The second level cache regions API.
105 */
106 private Cache|null $cache = null;
107
108 /**
109 * Creates a new EntityManager that operates on the given database connection
110 * and uses the given Configuration and EventManager implementations.
111 *
112 * @param Connection $conn The database connection used by the EntityManager.
113 */
114 public function __construct(
115 private Connection $conn,
116 private Configuration $config,
117 EventManager|null $eventManager = null,
118 ) {
119 if (! $config->getMetadataDriverImpl()) {
120 throw MissingMappingDriverImplementation::create();
121 }
122
123 $this->eventManager = $eventManager
124 ?? (method_exists($conn, 'getEventManager')
125 ? $conn->getEventManager()
126 : new EventManager()
127 );
128
129 $metadataFactoryClassName = $config->getClassMetadataFactoryName();
130
131 $this->metadataFactory = new $metadataFactoryClassName();
132 $this->metadataFactory->setEntityManager($this);
133
134 $this->configureMetadataCache();
135
136 $this->repositoryFactory = $config->getRepositoryFactory();
137 $this->unitOfWork = new UnitOfWork($this);
138 $this->proxyFactory = new ProxyFactory(
139 $this,
140 $config->getProxyDir(),
141 $config->getProxyNamespace(),
142 $config->getAutoGenerateProxyClasses(),
143 );
144
145 if ($config->isSecondLevelCacheEnabled()) {
146 $cacheConfig = $config->getSecondLevelCacheConfiguration();
147 $cacheFactory = $cacheConfig->getCacheFactory();
148 $this->cache = $cacheFactory->createCache($this);
149 }
150 }
151
152 public function getConnection(): Connection
153 {
154 return $this->conn;
155 }
156
157 public function getMetadataFactory(): ClassMetadataFactory
158 {
159 return $this->metadataFactory;
160 }
161
162 public function getExpressionBuilder(): Expr
163 {
164 return $this->expressionBuilder ??= new Expr();
165 }
166
167 public function beginTransaction(): void
168 {
169 $this->conn->beginTransaction();
170 }
171
172 public function getCache(): Cache|null
173 {
174 return $this->cache;
175 }
176
177 public function wrapInTransaction(callable $func): mixed
178 {
179 $this->conn->beginTransaction();
180
181 try {
182 $return = $func($this);
183
184 $this->flush();
185 $this->conn->commit();
186
187 return $return;
188 } catch (Throwable $e) {
189 $this->close();
190 $this->conn->rollBack();
191
192 throw $e;
193 }
194 }
195
196 public function commit(): void
197 {
198 $this->conn->commit();
199 }
200
201 public function rollback(): void
202 {
203 $this->conn->rollBack();
204 }
205
206 /**
207 * Returns the ORM metadata descriptor for a class.
208 *
209 * Internal note: Performance-sensitive method.
210 *
211 * {@inheritDoc}
212 */
213 public function getClassMetadata(string $className): Mapping\ClassMetadata
214 {
215 return $this->metadataFactory->getMetadataFor($className);
216 }
217
218 public function createQuery(string $dql = ''): Query
219 {
220 $query = new Query($this);
221
222 if (! empty($dql)) {
223 $query->setDQL($dql);
224 }
225
226 return $query;
227 }
228
229 public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery
230 {
231 $query = new NativeQuery($this);
232
233 $query->setSQL($sql);
234 $query->setResultSetMapping($rsm);
235
236 return $query;
237 }
238
239 public function createQueryBuilder(): QueryBuilder
240 {
241 return new QueryBuilder($this);
242 }
243
244 /**
245 * Flushes all changes to objects that have been queued up to now to the database.
246 * This effectively synchronizes the in-memory state of managed objects with the
247 * database.
248 *
249 * If an entity is explicitly passed to this method only this entity and
250 * the cascade-persist semantics + scheduled inserts/removals are synchronized.
251 *
252 * @throws OptimisticLockException If a version check on an entity that
253 * makes use of optimistic locking fails.
254 * @throws ORMException
255 */
256 public function flush(): void
257 {
258 $this->errorIfClosed();
259 $this->unitOfWork->commit();
260 }
261
262 /**
263 * {@inheritDoc}
264 */
265 public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
266 {
267 $class = $this->metadataFactory->getMetadataFor(ltrim($className, '\\'));
268
269 if ($lockMode !== null) {
270 $this->checkLockRequirements($lockMode, $class);
271 }
272
273 if (! is_array($id)) {
274 if ($class->isIdentifierComposite) {
275 throw ORMInvalidArgumentException::invalidCompositeIdentifier();
276 }
277
278 $id = [$class->identifier[0] => $id];
279 }
280
281 foreach ($id as $i => $value) {
282 if (is_object($value)) {
283 $className = DefaultProxyClassNameResolver::getClass($value);
284 if ($this->metadataFactory->hasMetadataFor($className)) {
285 $id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);
286
287 if ($id[$i] === null) {
288 throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className);
289 }
290 }
291 }
292 }
293
294 $sortedId = [];
295
296 foreach ($class->identifier as $identifier) {
297 if (! isset($id[$identifier])) {
298 throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
299 }
300
301 if ($id[$identifier] instanceof BackedEnum) {
302 $sortedId[$identifier] = $id[$identifier]->value;
303 } else {
304 $sortedId[$identifier] = $id[$identifier];
305 }
306
307 unset($id[$identifier]);
308 }
309
310 if ($id) {
311 throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
312 }
313
314 $unitOfWork = $this->getUnitOfWork();
315
316 $entity = $unitOfWork->tryGetById($sortedId, $class->rootEntityName);
317
318 // Check identity map first
319 if ($entity !== false) {
320 if (! ($entity instanceof $class->name)) {
321 return null;
322 }
323
324 switch (true) {
325 case $lockMode === LockMode::OPTIMISTIC:
326 $this->lock($entity, $lockMode, $lockVersion);
327 break;
328
329 case $lockMode === LockMode::NONE:
330 case $lockMode === LockMode::PESSIMISTIC_READ:
331 case $lockMode === LockMode::PESSIMISTIC_WRITE:
332 $persister = $unitOfWork->getEntityPersister($class->name);
333 $persister->refresh($sortedId, $entity, $lockMode);
334 break;
335 }
336
337 return $entity; // Hit!
338 }
339
340 $persister = $unitOfWork->getEntityPersister($class->name);
341
342 switch (true) {
343 case $lockMode === LockMode::OPTIMISTIC:
344 $entity = $persister->load($sortedId);
345
346 if ($entity !== null) {
347 $unitOfWork->lock($entity, $lockMode, $lockVersion);
348 }
349
350 return $entity;
351
352 case $lockMode === LockMode::PESSIMISTIC_READ:
353 case $lockMode === LockMode::PESSIMISTIC_WRITE:
354 return $persister->load($sortedId, null, null, [], $lockMode);
355
356 default:
357 return $persister->loadById($sortedId);
358 }
359 }
360
361 public function getReference(string $entityName, mixed $id): object|null
362 {
363 $class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
364
365 if (! is_array($id)) {
366 $id = [$class->identifier[0] => $id];
367 }
368
369 $sortedId = [];
370
371 foreach ($class->identifier as $identifier) {
372 if (! isset($id[$identifier])) {
373 throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
374 }
375
376 $sortedId[$identifier] = $id[$identifier];
377 unset($id[$identifier]);
378 }
379
380 if ($id) {
381 throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
382 }
383
384 $entity = $this->unitOfWork->tryGetById($sortedId, $class->rootEntityName);
385
386 // Check identity map first, if its already in there just return it.
387 if ($entity !== false) {
388 return $entity instanceof $class->name ? $entity : null;
389 }
390
391 if ($class->subClasses) {
392 return $this->find($entityName, $sortedId);
393 }
394
395 $entity = $this->proxyFactory->getProxy($class->name, $sortedId);
396
397 $this->unitOfWork->registerManaged($entity, $sortedId, []);
398
399 return $entity;
400 }
401
402 /**
403 * Clears the EntityManager. All entities that are currently managed
404 * by this EntityManager become detached.
405 */
406 public function clear(): void
407 {
408 $this->unitOfWork->clear();
409 }
410
411 public function close(): void
412 {
413 $this->clear();
414
415 $this->closed = true;
416 }
417
418 /**
419 * Tells the EntityManager to make an instance managed and persistent.
420 *
421 * The entity will be entered into the database at or before transaction
422 * commit or as a result of the flush operation.
423 *
424 * NOTE: The persist operation always considers entities that are not yet known to
425 * this EntityManager as NEW. Do not pass detached entities to the persist operation.
426 *
427 * @throws ORMInvalidArgumentException
428 * @throws ORMException
429 */
430 public function persist(object $object): void
431 {
432 $this->errorIfClosed();
433
434 $this->unitOfWork->persist($object);
435 }
436
437 /**
438 * Removes an entity instance.
439 *
440 * A removed entity will be removed from the database at or before transaction commit
441 * or as a result of the flush operation.
442 *
443 * @throws ORMInvalidArgumentException
444 * @throws ORMException
445 */
446 public function remove(object $object): void
447 {
448 $this->errorIfClosed();
449
450 $this->unitOfWork->remove($object);
451 }
452
453 public function refresh(object $object, LockMode|int|null $lockMode = null): void
454 {
455 $this->errorIfClosed();
456
457 $this->unitOfWork->refresh($object, $lockMode);
458 }
459
460 /**
461 * Detaches an entity from the EntityManager, causing a managed entity to
462 * become detached. Unflushed changes made to the entity if any
463 * (including removal of the entity), will not be synchronized to the database.
464 * Entities which previously referenced the detached entity will continue to
465 * reference it.
466 *
467 * @throws ORMInvalidArgumentException
468 */
469 public function detach(object $object): void
470 {
471 $this->unitOfWork->detach($object);
472 }
473
474 public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void
475 {
476 $this->unitOfWork->lock($entity, $lockMode, $lockVersion);
477 }
478
479 /**
480 * Gets the repository for an entity class.
481 *
482 * @psalm-param class-string<T> $className
483 *
484 * @psalm-return EntityRepository<T>
485 *
486 * @template T of object
487 */
488 public function getRepository(string $className): EntityRepository
489 {
490 return $this->repositoryFactory->getRepository($this, $className);
491 }
492
493 /**
494 * Determines whether an entity instance is managed in this EntityManager.
495 *
496 * @return bool TRUE if this EntityManager currently manages the given entity, FALSE otherwise.
497 */
498 public function contains(object $object): bool
499 {
500 return $this->unitOfWork->isScheduledForInsert($object)
501 || $this->unitOfWork->isInIdentityMap($object)
502 && ! $this->unitOfWork->isScheduledForDelete($object);
503 }
504
505 public function getEventManager(): EventManager
506 {
507 return $this->eventManager;
508 }
509
510 public function getConfiguration(): Configuration
511 {
512 return $this->config;
513 }
514
515 /**
516 * Throws an exception if the EntityManager is closed or currently not active.
517 *
518 * @throws EntityManagerClosed If the EntityManager is closed.
519 */
520 private function errorIfClosed(): void
521 {
522 if ($this->closed) {
523 throw EntityManagerClosed::create();
524 }
525 }
526
527 public function isOpen(): bool
528 {
529 return ! $this->closed;
530 }
531
532 public function getUnitOfWork(): UnitOfWork
533 {
534 return $this->unitOfWork;
535 }
536
537 public function newHydrator(string|int $hydrationMode): AbstractHydrator
538 {
539 return match ($hydrationMode) {
540 Query::HYDRATE_OBJECT => new Internal\Hydration\ObjectHydrator($this),
541 Query::HYDRATE_ARRAY => new Internal\Hydration\ArrayHydrator($this),
542 Query::HYDRATE_SCALAR => new Internal\Hydration\ScalarHydrator($this),
543 Query::HYDRATE_SINGLE_SCALAR => new Internal\Hydration\SingleScalarHydrator($this),
544 Query::HYDRATE_SIMPLEOBJECT => new Internal\Hydration\SimpleObjectHydrator($this),
545 Query::HYDRATE_SCALAR_COLUMN => new Internal\Hydration\ScalarColumnHydrator($this),
546 default => $this->createCustomHydrator((string) $hydrationMode),
547 };
548 }
549
550 public function getProxyFactory(): ProxyFactory
551 {
552 return $this->proxyFactory;
553 }
554
555 public function initializeObject(object $obj): void
556 {
557 $this->unitOfWork->initializeObject($obj);
558 }
559
560 /**
561 * {@inheritDoc}
562 */
563 public function isUninitializedObject($obj): bool
564 {
565 return $this->unitOfWork->isUninitializedObject($obj);
566 }
567
568 public function getFilters(): FilterCollection
569 {
570 return $this->filterCollection ??= new FilterCollection($this);
571 }
572
573 public function isFiltersStateClean(): bool
574 {
575 return $this->filterCollection === null || $this->filterCollection->isClean();
576 }
577
578 public function hasFilters(): bool
579 {
580 return $this->filterCollection !== null;
581 }
582
583 /**
584 * @psalm-param LockMode::* $lockMode
585 *
586 * @throws OptimisticLockException
587 * @throws TransactionRequiredException
588 */
589 private function checkLockRequirements(LockMode|int $lockMode, ClassMetadata $class): void
590 {
591 switch ($lockMode) {
592 case LockMode::OPTIMISTIC:
593 if (! $class->isVersioned) {
594 throw OptimisticLockException::notVersioned($class->name);
595 }
596
597 break;
598 case LockMode::PESSIMISTIC_READ:
599 case LockMode::PESSIMISTIC_WRITE:
600 if (! $this->getConnection()->isTransactionActive()) {
601 throw TransactionRequiredException::transactionRequired();
602 }
603 }
604 }
605
606 private function configureMetadataCache(): void
607 {
608 $metadataCache = $this->config->getMetadataCache();
609 if (! $metadataCache) {
610 return;
611 }
612
613 $this->metadataFactory->setCache($metadataCache);
614 }
615
616 private function createCustomHydrator(string $hydrationMode): AbstractHydrator
617 {
618 $class = $this->config->getCustomHydrationMode($hydrationMode);
619
620 if ($class !== null) {
621 return new $class($this);
622 }
623
624 throw InvalidHydrationMode::fromMode($hydrationMode);
625 }
626}
diff --git a/vendor/doctrine/orm/src/EntityManagerInterface.php b/vendor/doctrine/orm/src/EntityManagerInterface.php
new file mode 100644
index 0000000..cf3102b
--- /dev/null
+++ b/vendor/doctrine/orm/src/EntityManagerInterface.php
@@ -0,0 +1,242 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use DateTimeInterface;
8use Doctrine\Common\EventManager;
9use Doctrine\DBAL\Connection;
10use Doctrine\DBAL\LockMode;
11use Doctrine\ORM\Exception\ORMException;
12use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
13use Doctrine\ORM\Mapping\ClassMetadataFactory;
14use Doctrine\ORM\Proxy\ProxyFactory;
15use Doctrine\ORM\Query\Expr;
16use Doctrine\ORM\Query\FilterCollection;
17use Doctrine\ORM\Query\ResultSetMapping;
18use Doctrine\Persistence\ObjectManager;
19
20interface EntityManagerInterface extends ObjectManager
21{
22 /**
23 * {@inheritDoc}
24 *
25 * @psalm-param class-string<T> $className
26 *
27 * @psalm-return EntityRepository<T>
28 *
29 * @template T of object
30 */
31 public function getRepository(string $className): EntityRepository;
32
33 /**
34 * Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled.
35 */
36 public function getCache(): Cache|null;
37
38 /**
39 * Gets the database connection object used by the EntityManager.
40 */
41 public function getConnection(): Connection;
42
43 public function getMetadataFactory(): ClassMetadataFactory;
44
45 /**
46 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
47 *
48 * Example:
49 *
50 * <code>
51 * $qb = $em->createQueryBuilder();
52 * $expr = $em->getExpressionBuilder();
53 * $qb->select('u')->from('User', 'u')
54 * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2)));
55 * </code>
56 */
57 public function getExpressionBuilder(): Expr;
58
59 /**
60 * Starts a transaction on the underlying database connection.
61 */
62 public function beginTransaction(): void;
63
64 /**
65 * Executes a function in a transaction.
66 *
67 * The function gets passed this EntityManager instance as an (optional) parameter.
68 *
69 * {@link flush} is invoked prior to transaction commit.
70 *
71 * If an exception occurs during execution of the function or flushing or transaction commit,
72 * the transaction is rolled back, the EntityManager closed and the exception re-thrown.
73 *
74 * @psalm-param callable(self): T $func The function to execute transactionally.
75 *
76 * @return mixed The value returned from the closure.
77 * @psalm-return T
78 *
79 * @template T
80 */
81 public function wrapInTransaction(callable $func): mixed;
82
83 /**
84 * Commits a transaction on the underlying database connection.
85 */
86 public function commit(): void;
87
88 /**
89 * Performs a rollback on the underlying database connection.
90 */
91 public function rollback(): void;
92
93 /**
94 * Creates a new Query object.
95 *
96 * @param string $dql The DQL string.
97 */
98 public function createQuery(string $dql = ''): Query;
99
100 /**
101 * Creates a native SQL query.
102 */
103 public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery;
104
105 /**
106 * Create a QueryBuilder instance
107 */
108 public function createQueryBuilder(): QueryBuilder;
109
110 /**
111 * Finds an Entity by its identifier.
112 *
113 * @param string $className The class name of the entity to find.
114 * @param mixed $id The identity of the entity to find.
115 * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
116 * or NULL if no specific lock mode should be used
117 * during the search.
118 * @param int|null $lockVersion The version of the entity to find when using
119 * optimistic locking.
120 * @psalm-param class-string<T> $className
121 * @psalm-param LockMode::*|null $lockMode
122 *
123 * @return object|null The entity instance or NULL if the entity can not be found.
124 * @psalm-return T|null
125 *
126 * @throws OptimisticLockException
127 * @throws ORMInvalidArgumentException
128 * @throws TransactionRequiredException
129 * @throws ORMException
130 *
131 * @template T of object
132 */
133 public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null;
134
135 /**
136 * Refreshes the persistent state of an object from the database,
137 * overriding any local changes that have not yet been persisted.
138 *
139 * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
140 * or NULL if no specific lock mode should be used
141 * during the search.
142 * @psalm-param LockMode::*|null $lockMode
143 *
144 * @throws ORMInvalidArgumentException
145 * @throws ORMException
146 * @throws TransactionRequiredException
147 */
148 public function refresh(object $object, LockMode|int|null $lockMode = null): void;
149
150 /**
151 * Gets a reference to the entity identified by the given type and identifier
152 * without actually loading it, if the entity is not yet loaded.
153 *
154 * @param string $entityName The name of the entity type.
155 * @param mixed $id The entity identifier.
156 * @psalm-param class-string<T> $entityName
157 *
158 * @psalm-return T|null
159 *
160 * @throws ORMException
161 *
162 * @template T of object
163 */
164 public function getReference(string $entityName, mixed $id): object|null;
165
166 /**
167 * Closes the EntityManager. All entities that are currently managed
168 * by this EntityManager become detached. The EntityManager may no longer
169 * be used after it is closed.
170 */
171 public function close(): void;
172
173 /**
174 * Acquire a lock on the given entity.
175 *
176 * @psalm-param LockMode::* $lockMode
177 *
178 * @throws OptimisticLockException
179 * @throws PessimisticLockException
180 */
181 public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void;
182
183 /**
184 * Gets the EventManager used by the EntityManager.
185 */
186 public function getEventManager(): EventManager;
187
188 /**
189 * Gets the Configuration used by the EntityManager.
190 */
191 public function getConfiguration(): Configuration;
192
193 /**
194 * Check if the Entity manager is open or closed.
195 */
196 public function isOpen(): bool;
197
198 /**
199 * Gets the UnitOfWork used by the EntityManager to coordinate operations.
200 */
201 public function getUnitOfWork(): UnitOfWork;
202
203 /**
204 * Create a new instance for the given hydration mode.
205 *
206 * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
207 *
208 * @throws ORMException
209 */
210 public function newHydrator(string|int $hydrationMode): AbstractHydrator;
211
212 /**
213 * Gets the proxy factory used by the EntityManager to create entity proxies.
214 */
215 public function getProxyFactory(): ProxyFactory;
216
217 /**
218 * Gets the enabled filters.
219 */
220 public function getFilters(): FilterCollection;
221
222 /**
223 * Checks whether the state of the filter collection is clean.
224 */
225 public function isFiltersStateClean(): bool;
226
227 /**
228 * Checks whether the Entity Manager has filters.
229 */
230 public function hasFilters(): bool;
231
232 /**
233 * {@inheritDoc}
234 *
235 * @psalm-param string|class-string<T> $className
236 *
237 * @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
238 *
239 * @psalm-template T of object
240 */
241 public function getClassMetadata(string $className): Mapping\ClassMetadata;
242}
diff --git a/vendor/doctrine/orm/src/EntityNotFoundException.php b/vendor/doctrine/orm/src/EntityNotFoundException.php
new file mode 100644
index 0000000..142dc8a
--- /dev/null
+++ b/vendor/doctrine/orm/src/EntityNotFoundException.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Exception\ORMException;
8use RuntimeException;
9
10use function implode;
11use function sprintf;
12
13/**
14 * Exception thrown when a Proxy fails to retrieve an Entity result.
15 */
16class EntityNotFoundException extends RuntimeException implements ORMException
17{
18 /**
19 * Static constructor.
20 *
21 * @param string[] $id
22 */
23 public static function fromClassNameAndIdentifier(string $className, array $id): self
24 {
25 $ids = [];
26
27 foreach ($id as $key => $value) {
28 $ids[] = $key . '(' . $value . ')';
29 }
30
31 return new self(
32 'Entity of type \'' . $className . '\'' . ($ids ? ' for IDs ' . implode(', ', $ids) : '') . ' was not found',
33 );
34 }
35
36 /**
37 * Instance for which no identifier can be found
38 */
39 public static function noIdentifierFound(string $className): self
40 {
41 return new self(sprintf(
42 'Unable to find "%s" entity identifier associated with the UnitOfWork',
43 $className,
44 ));
45 }
46}
diff --git a/vendor/doctrine/orm/src/EntityRepository.php b/vendor/doctrine/orm/src/EntityRepository.php
new file mode 100644
index 0000000..a53c528
--- /dev/null
+++ b/vendor/doctrine/orm/src/EntityRepository.php
@@ -0,0 +1,236 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use BadMethodCallException;
8use Doctrine\Common\Collections\AbstractLazyCollection;
9use Doctrine\Common\Collections\Criteria;
10use Doctrine\Common\Collections\Selectable;
11use Doctrine\DBAL\LockMode;
12use Doctrine\Inflector\Inflector;
13use Doctrine\Inflector\InflectorFactory;
14use Doctrine\ORM\Mapping\ClassMetadata;
15use Doctrine\ORM\Query\ResultSetMappingBuilder;
16use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall;
17use Doctrine\Persistence\ObjectRepository;
18
19use function array_slice;
20use function lcfirst;
21use function sprintf;
22use function str_starts_with;
23use function substr;
24
25/**
26 * An EntityRepository serves as a repository for entities with generic as well as
27 * business specific methods for retrieving entities.
28 *
29 * This class is designed for inheritance and users can subclass this class to
30 * write their own repositories with business-specific methods to locate entities.
31 *
32 * @template T of object
33 * @template-implements Selectable<int,T>
34 * @template-implements ObjectRepository<T>
35 */
36class EntityRepository implements ObjectRepository, Selectable
37{
38 /** @psalm-var class-string<T> */
39 private readonly string $entityName;
40 private static Inflector|null $inflector = null;
41
42 /** @psalm-param ClassMetadata<T> $class */
43 public function __construct(
44 private readonly EntityManagerInterface $em,
45 private readonly ClassMetadata $class,
46 ) {
47 $this->entityName = $class->name;
48 }
49
50 /**
51 * Creates a new QueryBuilder instance that is prepopulated for this entity name.
52 */
53 public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder
54 {
55 return $this->em->createQueryBuilder()
56 ->select($alias)
57 ->from($this->entityName, $alias, $indexBy);
58 }
59
60 /**
61 * Creates a new result set mapping builder for this entity.
62 *
63 * The column naming strategy is "INCREMENT".
64 */
65 public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder
66 {
67 $rsm = new ResultSetMappingBuilder($this->em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
68 $rsm->addRootEntityFromClassMetadata($this->entityName, $alias);
69
70 return $rsm;
71 }
72
73 /**
74 * Finds an entity by its primary key / identifier.
75 *
76 * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
77 * or NULL if no specific lock mode should be used
78 * during the search.
79 * @psalm-param LockMode::*|null $lockMode
80 *
81 * @return object|null The entity instance or NULL if the entity can not be found.
82 * @psalm-return ?T
83 */
84 public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
85 {
86 return $this->em->find($this->entityName, $id, $lockMode, $lockVersion);
87 }
88
89 /**
90 * Finds all entities in the repository.
91 *
92 * @psalm-return list<T> The entities.
93 */
94 public function findAll(): array
95 {
96 return $this->findBy([]);
97 }
98
99 /**
100 * Finds entities by a set of criteria.
101 *
102 * {@inheritDoc}
103 *
104 * @psalm-return list<T>
105 */
106 public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array
107 {
108 $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
109
110 return $persister->loadAll($criteria, $orderBy, $limit, $offset);
111 }
112
113 /**
114 * Finds a single entity by a set of criteria.
115 *
116 * @psalm-param array<string, mixed> $criteria
117 * @psalm-param array<string, string>|null $orderBy
118 *
119 * @psalm-return T|null
120 */
121 public function findOneBy(array $criteria, array|null $orderBy = null): object|null
122 {
123 $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
124
125 return $persister->load($criteria, null, null, [], null, 1, $orderBy);
126 }
127
128 /**
129 * Counts entities by a set of criteria.
130 *
131 * @psalm-param array<string, mixed> $criteria
132 *
133 * @return int The cardinality of the objects that match the given criteria.
134 *
135 * @todo Add this method to `ObjectRepository` interface in the next major release
136 */
137 public function count(array $criteria = []): int
138 {
139 return $this->em->getUnitOfWork()->getEntityPersister($this->entityName)->count($criteria);
140 }
141
142 /**
143 * Adds support for magic method calls.
144 *
145 * @param mixed[] $arguments
146 * @psalm-param list<mixed> $arguments
147 *
148 * @throws BadMethodCallException If the method called is invalid.
149 */
150 public function __call(string $method, array $arguments): mixed
151 {
152 if (str_starts_with($method, 'findBy')) {
153 return $this->resolveMagicCall('findBy', substr($method, 6), $arguments);
154 }
155
156 if (str_starts_with($method, 'findOneBy')) {
157 return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments);
158 }
159
160 if (str_starts_with($method, 'countBy')) {
161 return $this->resolveMagicCall('count', substr($method, 7), $arguments);
162 }
163
164 throw new BadMethodCallException(sprintf(
165 'Undefined method "%s". The method name must start with ' .
166 'either findBy, findOneBy or countBy!',
167 $method,
168 ));
169 }
170
171 /** @psalm-return class-string<T> */
172 protected function getEntityName(): string
173 {
174 return $this->entityName;
175 }
176
177 public function getClassName(): string
178 {
179 return $this->getEntityName();
180 }
181
182 protected function getEntityManager(): EntityManagerInterface
183 {
184 return $this->em;
185 }
186
187 /** @psalm-return ClassMetadata<T> */
188 protected function getClassMetadata(): ClassMetadata
189 {
190 return $this->class;
191 }
192
193 /**
194 * Select all elements from a selectable that match the expression and
195 * return a new collection containing these elements.
196 *
197 * @psalm-return AbstractLazyCollection<int, T>&Selectable<int, T>
198 */
199 public function matching(Criteria $criteria): AbstractLazyCollection&Selectable
200 {
201 $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
202
203 return new LazyCriteriaCollection($persister, $criteria);
204 }
205
206 /**
207 * Resolves a magic method call to the proper existent method at `EntityRepository`.
208 *
209 * @param string $method The method to call
210 * @param string $by The property name used as condition
211 * @psalm-param list<mixed> $arguments The arguments to pass at method call
212 *
213 * @throws InvalidMagicMethodCall If the method called is invalid or the
214 * requested field/association does not exist.
215 */
216 private function resolveMagicCall(string $method, string $by, array $arguments): mixed
217 {
218 if (! $arguments) {
219 throw InvalidMagicMethodCall::onMissingParameter($method . $by);
220 }
221
222 self::$inflector ??= InflectorFactory::create()->build();
223
224 $fieldName = lcfirst(self::$inflector->classify($by));
225
226 if (! ($this->class->hasField($fieldName) || $this->class->hasAssociation($fieldName))) {
227 throw InvalidMagicMethodCall::becauseFieldNotFoundIn(
228 $this->entityName,
229 $fieldName,
230 $method . $by,
231 );
232 }
233
234 return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1));
235 }
236}
diff --git a/vendor/doctrine/orm/src/Event/ListenersInvoker.php b/vendor/doctrine/orm/src/Event/ListenersInvoker.php
new file mode 100644
index 0000000..c0c327e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/ListenersInvoker.php
@@ -0,0 +1,98 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\Common\EventArgs;
8use Doctrine\Common\EventManager;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Mapping\EntityListenerResolver;
12
13/**
14 * A method invoker based on entity lifecycle.
15 */
16class ListenersInvoker
17{
18 final public const INVOKE_NONE = 0;
19 final public const INVOKE_LISTENERS = 1;
20 final public const INVOKE_CALLBACKS = 2;
21 final public const INVOKE_MANAGER = 4;
22
23 /** The Entity listener resolver. */
24 private readonly EntityListenerResolver $resolver;
25
26 /** The EventManager used for dispatching events. */
27 private readonly EventManager $eventManager;
28
29 public function __construct(EntityManagerInterface $em)
30 {
31 $this->eventManager = $em->getEventManager();
32 $this->resolver = $em->getConfiguration()->getEntityListenerResolver();
33 }
34
35 /**
36 * Get the subscribed event systems
37 *
38 * @param ClassMetadata $metadata The entity metadata.
39 * @param string $eventName The entity lifecycle event.
40 *
41 * @psalm-return int-mask-of<self::INVOKE_*> Bitmask of subscribed event systems.
42 */
43 public function getSubscribedSystems(ClassMetadata $metadata, string $eventName): int
44 {
45 $invoke = self::INVOKE_NONE;
46
47 if (isset($metadata->lifecycleCallbacks[$eventName])) {
48 $invoke |= self::INVOKE_CALLBACKS;
49 }
50
51 if (isset($metadata->entityListeners[$eventName])) {
52 $invoke |= self::INVOKE_LISTENERS;
53 }
54
55 if ($this->eventManager->hasListeners($eventName)) {
56 $invoke |= self::INVOKE_MANAGER;
57 }
58
59 return $invoke;
60 }
61
62 /**
63 * Dispatches the lifecycle event of the given entity.
64 *
65 * @param ClassMetadata $metadata The entity metadata.
66 * @param string $eventName The entity lifecycle event.
67 * @param object $entity The Entity on which the event occurred.
68 * @param EventArgs $event The Event args.
69 * @psalm-param int-mask-of<self::INVOKE_*> $invoke Bitmask to invoke listeners.
70 */
71 public function invoke(
72 ClassMetadata $metadata,
73 string $eventName,
74 object $entity,
75 EventArgs $event,
76 int $invoke,
77 ): void {
78 if ($invoke & self::INVOKE_CALLBACKS) {
79 foreach ($metadata->lifecycleCallbacks[$eventName] as $callback) {
80 $entity->$callback($event);
81 }
82 }
83
84 if ($invoke & self::INVOKE_LISTENERS) {
85 foreach ($metadata->entityListeners[$eventName] as $listener) {
86 $class = $listener['class'];
87 $method = $listener['method'];
88 $instance = $this->resolver->resolve($class);
89
90 $instance->$method($entity, $event);
91 }
92 }
93
94 if ($invoke & self::INVOKE_MANAGER) {
95 $this->eventManager->dispatchEvent($eventName, $event);
96 }
97 }
98}
diff --git a/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php b/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php
new file mode 100644
index 0000000..b450616
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\Persistence\Event\LoadClassMetadataEventArgs as BaseLoadClassMetadataEventArgs;
10
11/**
12 * Class that holds event arguments for a loadMetadata event.
13 *
14 * @extends BaseLoadClassMetadataEventArgs<ClassMetadata<object>, EntityManagerInterface>
15 */
16class LoadClassMetadataEventArgs extends BaseLoadClassMetadataEventArgs
17{
18 /**
19 * Retrieve associated EntityManager.
20 */
21 public function getEntityManager(): EntityManagerInterface
22 {
23 return $this->getObjectManager();
24 }
25}
diff --git a/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php b/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php
new file mode 100644
index 0000000..762c083
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php
@@ -0,0 +1,49 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\ManagerEventArgs;
9use Doctrine\Persistence\Mapping\ClassMetadata;
10use Doctrine\Persistence\ObjectManager;
11
12/**
13 * Class that holds event arguments for a `onClassMetadataNotFound` event.
14 *
15 * This object is mutable by design, allowing callbacks having access to it to set the
16 * found metadata in it, and therefore "cancelling" a `onClassMetadataNotFound` event
17 *
18 * @extends ManagerEventArgs<EntityManagerInterface>
19 */
20class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs
21{
22 private ClassMetadata|null $foundMetadata = null;
23
24 /** @param EntityManagerInterface $objectManager */
25 public function __construct(
26 private readonly string $className,
27 ObjectManager $objectManager,
28 ) {
29 parent::__construct($objectManager);
30 }
31
32 public function setFoundMetadata(ClassMetadata|null $classMetadata): void
33 {
34 $this->foundMetadata = $classMetadata;
35 }
36
37 public function getFoundMetadata(): ClassMetadata|null
38 {
39 return $this->foundMetadata;
40 }
41
42 /**
43 * Retrieve class name for which a failed metadata fetch attempt was executed
44 */
45 public function getClassName(): string
46 {
47 return $this->className;
48 }
49}
diff --git a/vendor/doctrine/orm/src/Event/OnClearEventArgs.php b/vendor/doctrine/orm/src/Event/OnClearEventArgs.php
new file mode 100644
index 0000000..29a42f2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/OnClearEventArgs.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\OnClearEventArgs as BaseOnClearEventArgs;
9
10/**
11 * Provides event arguments for the onClear event.
12 *
13 * @link www.doctrine-project.org
14 *
15 * @extends BaseOnClearEventArgs<EntityManagerInterface>
16 */
17class OnClearEventArgs extends BaseOnClearEventArgs
18{
19}
diff --git a/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php b/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php
new file mode 100644
index 0000000..b0594ca
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\ManagerEventArgs;
9
10/**
11 * Provides event arguments for the preFlush event.
12 *
13 * @link www.doctrine-project.org
14 *
15 * @extends ManagerEventArgs<EntityManagerInterface>
16 */
17class OnFlushEventArgs extends ManagerEventArgs
18{
19}
diff --git a/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php b/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php
new file mode 100644
index 0000000..ca41ba8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\ManagerEventArgs;
9
10/**
11 * Provides event arguments for the postFlush event.
12 *
13 * @link www.doctrine-project.org
14 *
15 * @extends ManagerEventArgs<EntityManagerInterface>
16 */
17class PostFlushEventArgs extends ManagerEventArgs
18{
19}
diff --git a/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php b/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php
new file mode 100644
index 0000000..8344e68
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PostLoadEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php b/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php
new file mode 100644
index 0000000..926ac1c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PostPersistEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php b/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php
new file mode 100644
index 0000000..8bf857e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PostRemoveEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php b/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php
new file mode 100644
index 0000000..c9ff004
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PostUpdateEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php b/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php
new file mode 100644
index 0000000..671535c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\ManagerEventArgs;
9
10/**
11 * Provides event arguments for the preFlush event.
12 *
13 * @link www.doctrine-project.com
14 *
15 * @extends ManagerEventArgs<EntityManagerInterface>
16 */
17class PreFlushEventArgs extends ManagerEventArgs
18{
19}
diff --git a/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php b/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php
new file mode 100644
index 0000000..e70c3cf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PrePersistEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php b/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php
new file mode 100644
index 0000000..3af0d02
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Event\LifecycleEventArgs;
9
10/** @extends LifecycleEventArgs<EntityManagerInterface> */
11final class PreRemoveEventArgs extends LifecycleEventArgs
12{
13}
diff --git a/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php b/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php
new file mode 100644
index 0000000..090487b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php
@@ -0,0 +1,100 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Event;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\PersistentCollection;
9use Doctrine\Persistence\Event\LifecycleEventArgs;
10use InvalidArgumentException;
11
12use function get_debug_type;
13use function sprintf;
14
15/**
16 * Class that holds event arguments for a preUpdate event.
17 *
18 * @extends LifecycleEventArgs<EntityManagerInterface>
19 */
20class PreUpdateEventArgs extends LifecycleEventArgs
21{
22 /** @var array<string, array{mixed, mixed}|PersistentCollection> */
23 private array $entityChangeSet;
24
25 /**
26 * @param mixed[][] $changeSet
27 * @psalm-param array<string, array{mixed, mixed}|PersistentCollection> $changeSet
28 */
29 public function __construct(object $entity, EntityManagerInterface $em, array &$changeSet)
30 {
31 parent::__construct($entity, $em);
32
33 $this->entityChangeSet = &$changeSet;
34 }
35
36 /**
37 * Retrieves entity changeset.
38 *
39 * @return mixed[][]
40 * @psalm-return array<string, array{mixed, mixed}|PersistentCollection>
41 */
42 public function getEntityChangeSet(): array
43 {
44 return $this->entityChangeSet;
45 }
46
47 /**
48 * Checks if field has a changeset.
49 */
50 public function hasChangedField(string $field): bool
51 {
52 return isset($this->entityChangeSet[$field]);
53 }
54
55 /**
56 * Gets the old value of the changeset of the changed field.
57 */
58 public function getOldValue(string $field): mixed
59 {
60 $this->assertValidField($field);
61
62 return $this->entityChangeSet[$field][0];
63 }
64
65 /**
66 * Gets the new value of the changeset of the changed field.
67 */
68 public function getNewValue(string $field): mixed
69 {
70 $this->assertValidField($field);
71
72 return $this->entityChangeSet[$field][1];
73 }
74
75 /**
76 * Sets the new value of this field.
77 */
78 public function setNewValue(string $field, mixed $value): void
79 {
80 $this->assertValidField($field);
81
82 $this->entityChangeSet[$field][1] = $value;
83 }
84
85 /**
86 * Asserts the field exists in changeset.
87 *
88 * @throws InvalidArgumentException
89 */
90 private function assertValidField(string $field): void
91 {
92 if (! isset($this->entityChangeSet[$field])) {
93 throw new InvalidArgumentException(sprintf(
94 'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.',
95 $field,
96 get_debug_type($this->getObject()),
97 ));
98 }
99 }
100}
diff --git a/vendor/doctrine/orm/src/Events.php b/vendor/doctrine/orm/src/Events.php
new file mode 100644
index 0000000..517917c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Events.php
@@ -0,0 +1,125 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7/**
8 * Container for all ORM events.
9 *
10 * This class cannot be instantiated.
11 */
12final class Events
13{
14 /**
15 * Private constructor. This class is not meant to be instantiated.
16 */
17 private function __construct()
18 {
19 }
20
21 /**
22 * The preRemove event occurs for a given entity before the respective
23 * EntityManager remove operation for that entity is executed.
24 *
25 * This is an entity lifecycle event.
26 */
27 public const preRemove = 'preRemove';
28
29 /**
30 * The postRemove event occurs for an entity after the entity has
31 * been deleted. It will be invoked after the database delete operations.
32 *
33 * This is an entity lifecycle event.
34 */
35 public const postRemove = 'postRemove';
36
37 /**
38 * The prePersist event occurs for a given entity before the respective
39 * EntityManager persist operation for that entity is executed.
40 *
41 * This is an entity lifecycle event.
42 */
43 public const prePersist = 'prePersist';
44
45 /**
46 * The postPersist event occurs for an entity after the entity has
47 * been made persistent. It will be invoked after the database insert operations.
48 * Generated primary key values are available in the postPersist event.
49 *
50 * This is an entity lifecycle event.
51 */
52 public const postPersist = 'postPersist';
53
54 /**
55 * The preUpdate event occurs before the database update operations to
56 * entity data.
57 *
58 * This is an entity lifecycle event.
59 */
60 public const preUpdate = 'preUpdate';
61
62 /**
63 * The postUpdate event occurs after the database update operations to
64 * entity data.
65 *
66 * This is an entity lifecycle event.
67 */
68 public const postUpdate = 'postUpdate';
69
70 /**
71 * The postLoad event occurs for an entity after the entity has been loaded
72 * into the current EntityManager from the database or after the refresh operation
73 * has been applied to it.
74 *
75 * Note that the postLoad event occurs for an entity before any associations have been
76 * initialized. Therefore, it is not safe to access associations in a postLoad callback
77 * or event handler.
78 *
79 * This is an entity lifecycle event.
80 */
81 public const postLoad = 'postLoad';
82
83 /**
84 * The loadClassMetadata event occurs after the mapping metadata for a class
85 * has been loaded from a mapping source (attributes/xml).
86 */
87 public const loadClassMetadata = 'loadClassMetadata';
88
89 /**
90 * The onClassMetadataNotFound event occurs whenever loading metadata for a class
91 * failed.
92 */
93 public const onClassMetadataNotFound = 'onClassMetadataNotFound';
94
95 /**
96 * The preFlush event occurs when the EntityManager#flush() operation is invoked,
97 * but before any changes to managed entities have been calculated. This event is
98 * always raised right after EntityManager#flush() call.
99 */
100 public const preFlush = 'preFlush';
101
102 /**
103 * The onFlush event occurs when the EntityManager#flush() operation is invoked,
104 * after any changes to managed entities have been determined but before any
105 * actual database operations are executed. The event is only raised if there is
106 * actually something to do for the underlying UnitOfWork. If nothing needs to be done,
107 * the onFlush event is not raised.
108 */
109 public const onFlush = 'onFlush';
110
111 /**
112 * The postFlush event occurs when the EntityManager#flush() operation is invoked and
113 * after all actual database operations are executed successfully. The event is only raised if there is
114 * actually something to do for the underlying UnitOfWork. If nothing needs to be done,
115 * the postFlush event is not raised. The event won't be raised if an error occurs during the
116 * flush operation.
117 */
118 public const postFlush = 'postFlush';
119
120 /**
121 * The onClear event occurs when the EntityManager#clear() operation is invoked,
122 * after all references to entities have been removed from the unit of work.
123 */
124 public const onClear = 'onClear';
125}
diff --git a/vendor/doctrine/orm/src/Exception/ConfigurationException.php b/vendor/doctrine/orm/src/Exception/ConfigurationException.php
new file mode 100644
index 0000000..45cf83f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/ConfigurationException.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7interface ConfigurationException extends ORMException
8{
9}
diff --git a/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php b/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php
new file mode 100644
index 0000000..0af3162
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php
@@ -0,0 +1,39 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Exception;
8
9use function sprintf;
10
11final class EntityIdentityCollisionException extends Exception implements ORMException
12{
13 public static function create(object $existingEntity, object $newEntity, string $idHash): self
14 {
15 return new self(
16 sprintf(
17 <<<'EXCEPTION'
18While adding an entity of class %s with an ID hash of "%s" to the identity map,
19another object of class %s was already present for the same ID. This exception
20is a safeguard against an internal inconsistency - IDs should uniquely map to
21entity object instances. This problem may occur if:
22
23- you use application-provided IDs and reuse ID values;
24- database-provided IDs are reassigned after truncating the database without
25clearing the EntityManager;
26- you might have been using EntityManager#getReference() to create a reference
27for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
28entity.
29
30Otherwise, it might be an ORM-internal inconsistency, please report it.
31EXCEPTION
32 ,
33 $newEntity::class,
34 $idHash,
35 $existingEntity::class,
36 ),
37 );
38 }
39}
diff --git a/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php b/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php
new file mode 100644
index 0000000..a769202
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use RuntimeException;
8
9final class EntityManagerClosed extends RuntimeException implements ManagerException
10{
11 public static function create(): self
12 {
13 return new self('The EntityManager is closed.');
14 }
15}
diff --git a/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php b/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php
new file mode 100644
index 0000000..d566436
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function get_debug_type;
10
11final class EntityMissingAssignedId extends LogicException implements ORMException
12{
13 public static function forField(object $entity, string $field): self
14 {
15 return new self('Entity of type ' . get_debug_type($entity) . " is missing an assigned ID for field '" . $field . "'. " .
16 'The identifier generation strategy for this entity requires the ID field to be populated before ' .
17 'EntityManager#persist() is called. If you want automatically generated identifiers instead ' .
18 'you need to adjust the metadata mapping accordingly.');
19 }
20}
diff --git a/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php b/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php
new file mode 100644
index 0000000..c28143d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Doctrine\ORM\EntityRepository;
8use LogicException;
9
10final class InvalidEntityRepository extends LogicException implements ConfigurationException
11{
12 public static function fromClassName(string $className): self
13 {
14 return new self(
15 "Invalid repository class '" . $className . "'. It must be a " . EntityRepository::class . '.',
16 );
17 }
18}
diff --git a/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php b/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php
new file mode 100644
index 0000000..d07d84c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function sprintf;
10
11final class InvalidHydrationMode extends LogicException implements ManagerException
12{
13 public static function fromMode(string $mode): self
14 {
15 return new self(sprintf('"%s" is an invalid hydration mode.', $mode));
16 }
17}
diff --git a/vendor/doctrine/orm/src/Exception/ManagerException.php b/vendor/doctrine/orm/src/Exception/ManagerException.php
new file mode 100644
index 0000000..f9bc7ff
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/ManagerException.php
@@ -0,0 +1,11 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Throwable;
8
9interface ManagerException extends Throwable
10{
11}
diff --git a/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php b/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php
new file mode 100644
index 0000000..2c02db4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function sprintf;
10
11final class MissingIdentifierField extends LogicException implements ManagerException
12{
13 public static function fromFieldAndClass(string $fieldName, string $className): self
14 {
15 return new self(sprintf(
16 'The identifier %s is missing for a query of %s',
17 $fieldName,
18 $className,
19 ));
20 }
21}
diff --git a/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php b/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php
new file mode 100644
index 0000000..ce5104b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9final class MissingMappingDriverImplementation extends LogicException implements ManagerException
10{
11 public static function create(): self
12 {
13 return new self(
14 "It's a requirement to specify a Metadata Driver and pass it " .
15 'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().',
16 );
17 }
18}
diff --git a/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php b/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php
new file mode 100644
index 0000000..8084d66
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function implode;
10use function sprintf;
11
12final class MultipleSelectorsFoundException extends LogicException implements ORMException
13{
14 public const MULTIPLE_SELECTORS_FOUND_EXCEPTION = 'Multiple selectors found: %s. Please select only one.';
15
16 /** @param string[] $selectors */
17 public static function create(array $selectors): self
18 {
19 return new self(
20 sprintf(
21 self::MULTIPLE_SELECTORS_FOUND_EXCEPTION,
22 implode(', ', $selectors),
23 ),
24 );
25 }
26}
diff --git a/vendor/doctrine/orm/src/Exception/NotSupported.php b/vendor/doctrine/orm/src/Exception/NotSupported.php
new file mode 100644
index 0000000..9192f87
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/NotSupported.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function sprintf;
10
11/** @deprecated */
12final class NotSupported extends LogicException implements ORMException
13{
14 public static function create(): self
15 {
16 return new self('This behaviour is (currently) not supported by Doctrine 2');
17 }
18
19 public static function createForDbal3(string $context): self
20 {
21 return new self(sprintf(
22 <<<'EXCEPTION'
23Context: %s
24Problem: Feature was deprecated in doctrine/dbal 2.x and is not supported by installed doctrine/dbal:3.x
25Solution: See the doctrine/deprecations logs for new alternative approaches.
26EXCEPTION
27 ,
28 $context,
29 ));
30 }
31
32 public static function createForPersistence3(string $context): self
33 {
34 return new self(sprintf(
35 <<<'EXCEPTION'
36Context: %s
37Problem: Feature was deprecated in doctrine/persistence 2.x and is not supported by installed doctrine/persistence:3.x
38Solution: See the doctrine/deprecations logs for new alternative approaches.
39EXCEPTION
40 ,
41 $context,
42 ));
43 }
44}
diff --git a/vendor/doctrine/orm/src/Exception/ORMException.php b/vendor/doctrine/orm/src/Exception/ORMException.php
new file mode 100644
index 0000000..a59b483
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/ORMException.php
@@ -0,0 +1,11 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Throwable;
8
9interface ORMException extends Throwable
10{
11}
diff --git a/vendor/doctrine/orm/src/Exception/PersisterException.php b/vendor/doctrine/orm/src/Exception/PersisterException.php
new file mode 100644
index 0000000..2563a46
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/PersisterException.php
@@ -0,0 +1,11 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Doctrine\ORM\Persisters\PersisterException as BasePersisterException;
8
9class PersisterException extends BasePersisterException
10{
11}
diff --git a/vendor/doctrine/orm/src/Exception/RepositoryException.php b/vendor/doctrine/orm/src/Exception/RepositoryException.php
new file mode 100644
index 0000000..3b2d51b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/RepositoryException.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7/**
8 * This interface should be implemented by all exceptions in the Repository
9 * namespace.
10 */
11interface RepositoryException extends ORMException
12{
13}
diff --git a/vendor/doctrine/orm/src/Exception/SchemaToolException.php b/vendor/doctrine/orm/src/Exception/SchemaToolException.php
new file mode 100644
index 0000000..e4477d0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/SchemaToolException.php
@@ -0,0 +1,11 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Throwable;
8
9interface SchemaToolException extends Throwable
10{
11}
diff --git a/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php b/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php
new file mode 100644
index 0000000..cf3ca18
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use Doctrine\ORM\Cache\Exception\CacheException;
8
9use function sprintf;
10
11final class UnexpectedAssociationValue extends CacheException
12{
13 public static function create(
14 string $class,
15 string $association,
16 string $given,
17 string $expected,
18 ): self {
19 return new self(sprintf(
20 'Found entity of type %s on association %s#%s, but expecting %s',
21 $given,
22 $class,
23 $association,
24 $expected,
25 ));
26 }
27}
diff --git a/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php b/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php
new file mode 100644
index 0000000..645bdae
--- /dev/null
+++ b/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Exception;
6
7use LogicException;
8
9use function implode;
10use function sprintf;
11
12final class UnrecognizedIdentifierFields extends LogicException implements ManagerException
13{
14 /** @param string[] $fieldNames */
15 public static function fromClassAndFieldNames(string $className, array $fieldNames): self
16 {
17 return new self(sprintf(
18 'Unrecognized identifier fields: "%s" are not present on class "%s".',
19 implode("', '", $fieldNames),
20 $className,
21 ));
22 }
23}
diff --git a/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php b/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php
new file mode 100644
index 0000000..6d981f8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Id;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9abstract class AbstractIdGenerator
10{
11 /**
12 * Generates an identifier for an entity.
13 */
14 abstract public function generateId(EntityManagerInterface $em, object|null $entity): mixed;
15
16 /**
17 * Gets whether this generator is a post-insert generator which means that
18 * {@link generateId()} must be called after the entity has been inserted
19 * into the database.
20 *
21 * By default, this method returns FALSE. Generators that have this requirement
22 * must override this method and return TRUE.
23 */
24 public function isPostInsertGenerator(): bool
25 {
26 return false;
27 }
28}
diff --git a/vendor/doctrine/orm/src/Id/AssignedGenerator.php b/vendor/doctrine/orm/src/Id/AssignedGenerator.php
new file mode 100644
index 0000000..e11b341
--- /dev/null
+++ b/vendor/doctrine/orm/src/Id/AssignedGenerator.php
@@ -0,0 +1,45 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Id;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Exception\EntityMissingAssignedId;
9
10/**
11 * Special generator for application-assigned identifiers (doesn't really generate anything).
12 */
13class AssignedGenerator extends AbstractIdGenerator
14{
15 /**
16 * Returns the identifier assigned to the given entity.
17 *
18 * {@inheritDoc}
19 *
20 * @throws EntityMissingAssignedId
21 */
22 public function generateId(EntityManagerInterface $em, object|null $entity): array
23 {
24 $class = $em->getClassMetadata($entity::class);
25 $idFields = $class->getIdentifierFieldNames();
26 $identifier = [];
27
28 foreach ($idFields as $idField) {
29 $value = $class->getFieldValue($entity, $idField);
30
31 if (! isset($value)) {
32 throw EntityMissingAssignedId::forField($entity, $idField);
33 }
34
35 if (isset($class->associationMappings[$idField])) {
36 // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
37 $value = $em->getUnitOfWork()->getSingleIdentifierValue($value);
38 }
39
40 $identifier[$idField] = $value;
41 }
42
43 return $identifier;
44 }
45}
diff --git a/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php b/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php
new file mode 100644
index 0000000..762a7cb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Id;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9/**
10 * Id generator that obtains IDs from special "identity" columns. These are columns
11 * that automatically get a database-generated, auto-incremented identifier on INSERT.
12 * This generator obtains the last insert id after such an insert.
13 */
14class BigIntegerIdentityGenerator extends AbstractIdGenerator
15{
16 public function generateId(EntityManagerInterface $em, object|null $entity): string
17 {
18 return (string) $em->getConnection()->lastInsertId();
19 }
20
21 public function isPostInsertGenerator(): bool
22 {
23 return true;
24 }
25}
diff --git a/vendor/doctrine/orm/src/Id/IdentityGenerator.php b/vendor/doctrine/orm/src/Id/IdentityGenerator.php
new file mode 100644
index 0000000..4610f66
--- /dev/null
+++ b/vendor/doctrine/orm/src/Id/IdentityGenerator.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Id;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9/**
10 * Id generator that obtains IDs from special "identity" columns. These are columns
11 * that automatically get a database-generated, auto-incremented identifier on INSERT.
12 * This generator obtains the last insert id after such an insert.
13 */
14class IdentityGenerator extends AbstractIdGenerator
15{
16 public function generateId(EntityManagerInterface $em, object|null $entity): int
17 {
18 return (int) $em->getConnection()->lastInsertId();
19 }
20
21 public function isPostInsertGenerator(): bool
22 {
23 return true;
24 }
25}
diff --git a/vendor/doctrine/orm/src/Id/SequenceGenerator.php b/vendor/doctrine/orm/src/Id/SequenceGenerator.php
new file mode 100644
index 0000000..659bb58
--- /dev/null
+++ b/vendor/doctrine/orm/src/Id/SequenceGenerator.php
@@ -0,0 +1,112 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Id;
6
7use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
8use Doctrine\Deprecations\Deprecation;
9use Doctrine\ORM\EntityManagerInterface;
10use Serializable;
11
12use function serialize;
13use function unserialize;
14
15/**
16 * Represents an ID generator that uses a database sequence.
17 */
18class SequenceGenerator extends AbstractIdGenerator implements Serializable
19{
20 private int $nextValue = 0;
21 private int|null $maxValue = null;
22
23 /**
24 * Initializes a new sequence generator.
25 *
26 * @param string $sequenceName The name of the sequence.
27 * @param int $allocationSize The allocation size of the sequence.
28 */
29 public function __construct(
30 private string $sequenceName,
31 private int $allocationSize,
32 ) {
33 }
34
35 public function generateId(EntityManagerInterface $em, object|null $entity): int
36 {
37 if ($this->maxValue === null || $this->nextValue === $this->maxValue) {
38 // Allocate new values
39 $connection = $em->getConnection();
40 $sql = $connection->getDatabasePlatform()->getSequenceNextValSQL($this->sequenceName);
41
42 if ($connection instanceof PrimaryReadReplicaConnection) {
43 $connection->ensureConnectedToPrimary();
44 }
45
46 $this->nextValue = (int) $connection->fetchOne($sql);
47 $this->maxValue = $this->nextValue + $this->allocationSize;
48 }
49
50 return $this->nextValue++;
51 }
52
53 /**
54 * Gets the maximum value of the currently allocated bag of values.
55 */
56 public function getCurrentMaxValue(): int|null
57 {
58 return $this->maxValue;
59 }
60
61 /**
62 * Gets the next value that will be returned by generate().
63 */
64 public function getNextValue(): int
65 {
66 return $this->nextValue;
67 }
68
69 /** @deprecated without replacement. */
70 final public function serialize(): string
71 {
72 Deprecation::trigger(
73 'doctrine/orm',
74 'https://github.com/doctrine/orm/pull/11468',
75 '%s() is deprecated, use __serialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
76 __METHOD__,
77 self::class,
78 );
79
80 return serialize($this->__serialize());
81 }
82
83 /** @return array<string, mixed> */
84 public function __serialize(): array
85 {
86 return [
87 'allocationSize' => $this->allocationSize,
88 'sequenceName' => $this->sequenceName,
89 ];
90 }
91
92 /** @deprecated without replacement. */
93 final public function unserialize(string $serialized): void
94 {
95 Deprecation::trigger(
96 'doctrine/orm',
97 'https://github.com/doctrine/orm/pull/11468',
98 '%s() is deprecated, use __unserialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.',
99 __METHOD__,
100 self::class,
101 );
102
103 $this->__unserialize(unserialize($serialized));
104 }
105
106 /** @param array<string, mixed> $data */
107 public function __unserialize(array $data): void
108 {
109 $this->sequenceName = $data['sequenceName'];
110 $this->allocationSize = $data['allocationSize'];
111 }
112}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php
new file mode 100644
index 0000000..d8bffe4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php
@@ -0,0 +1,556 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use BackedEnum;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Result;
10use Doctrine\DBAL\Types\Type;
11use Doctrine\ORM\EntityManagerInterface;
12use Doctrine\ORM\Events;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Query\ResultSetMapping;
15use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
16use Doctrine\ORM\UnitOfWork;
17use Generator;
18use LogicException;
19use ReflectionClass;
20
21use function array_map;
22use function array_merge;
23use function count;
24use function end;
25use function in_array;
26use function is_array;
27
28/**
29 * Base class for all hydrators. A hydrator is a class that provides some form
30 * of transformation of an SQL result set into another structure.
31 *
32 * @psalm-consistent-constructor
33 */
34abstract class AbstractHydrator
35{
36 /**
37 * The ResultSetMapping.
38 */
39 protected ResultSetMapping|null $rsm = null;
40
41 /**
42 * The dbms Platform instance.
43 */
44 protected AbstractPlatform $platform;
45
46 /**
47 * The UnitOfWork of the associated EntityManager.
48 */
49 protected UnitOfWork $uow;
50
51 /**
52 * Local ClassMetadata cache to avoid going to the EntityManager all the time.
53 *
54 * @var array<string, ClassMetadata<object>>
55 */
56 protected array $metadataCache = [];
57
58 /**
59 * The cache used during row-by-row hydration.
60 *
61 * @var array<string, mixed[]|null>
62 */
63 protected array $cache = [];
64
65 /**
66 * The statement that provides the data to hydrate.
67 */
68 protected Result|null $stmt = null;
69
70 /**
71 * The query hints.
72 *
73 * @var array<string, mixed>
74 */
75 protected array $hints = [];
76
77 /**
78 * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
79 */
80 public function __construct(protected EntityManagerInterface $em)
81 {
82 $this->platform = $em->getConnection()->getDatabasePlatform();
83 $this->uow = $em->getUnitOfWork();
84 }
85
86 /**
87 * Initiates a row-by-row hydration.
88 *
89 * @psalm-param array<string, mixed> $hints
90 *
91 * @return Generator<array-key, mixed>
92 *
93 * @final
94 */
95 final public function toIterable(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): Generator
96 {
97 $this->stmt = $stmt;
98 $this->rsm = $resultSetMapping;
99 $this->hints = $hints;
100
101 $evm = $this->em->getEventManager();
102
103 $evm->addEventListener([Events::onClear], $this);
104
105 $this->prepare();
106
107 try {
108 while (true) {
109 $row = $this->statement()->fetchAssociative();
110
111 if ($row === false) {
112 break;
113 }
114
115 $result = [];
116
117 $this->hydrateRowData($row, $result);
118
119 $this->cleanupAfterRowIteration();
120 if (count($result) === 1) {
121 if (count($resultSetMapping->indexByMap) === 0) {
122 yield end($result);
123 } else {
124 yield from $result;
125 }
126 } else {
127 yield $result;
128 }
129 }
130 } finally {
131 $this->cleanup();
132 }
133 }
134
135 final protected function statement(): Result
136 {
137 if ($this->stmt === null) {
138 throw new LogicException('Uninitialized _stmt property');
139 }
140
141 return $this->stmt;
142 }
143
144 final protected function resultSetMapping(): ResultSetMapping
145 {
146 if ($this->rsm === null) {
147 throw new LogicException('Uninitialized _rsm property');
148 }
149
150 return $this->rsm;
151 }
152
153 /**
154 * Hydrates all rows returned by the passed statement instance at once.
155 *
156 * @psalm-param array<string, string> $hints
157 */
158 public function hydrateAll(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): mixed
159 {
160 $this->stmt = $stmt;
161 $this->rsm = $resultSetMapping;
162 $this->hints = $hints;
163
164 $this->em->getEventManager()->addEventListener([Events::onClear], $this);
165 $this->prepare();
166
167 try {
168 $result = $this->hydrateAllData();
169 } finally {
170 $this->cleanup();
171 }
172
173 return $result;
174 }
175
176 /**
177 * When executed in a hydrate() loop we have to clear internal state to
178 * decrease memory consumption.
179 */
180 public function onClear(mixed $eventArgs): void
181 {
182 }
183
184 /**
185 * Executes one-time preparation tasks, once each time hydration is started
186 * through {@link hydrateAll} or {@link toIterable()}.
187 */
188 protected function prepare(): void
189 {
190 }
191
192 /**
193 * Executes one-time cleanup tasks at the end of a hydration that was initiated
194 * through {@link hydrateAll} or {@link toIterable()}.
195 */
196 protected function cleanup(): void
197 {
198 $this->statement()->free();
199
200 $this->stmt = null;
201 $this->rsm = null;
202 $this->cache = [];
203 $this->metadataCache = [];
204
205 $this
206 ->em
207 ->getEventManager()
208 ->removeEventListener([Events::onClear], $this);
209 }
210
211 protected function cleanupAfterRowIteration(): void
212 {
213 }
214
215 /**
216 * Hydrates a single row from the current statement instance.
217 *
218 * Template method.
219 *
220 * @param mixed[] $row The row data.
221 * @param mixed[] $result The result to fill.
222 *
223 * @throws HydrationException
224 */
225 protected function hydrateRowData(array $row, array &$result): void
226 {
227 throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
228 }
229
230 /**
231 * Hydrates all rows from the current statement instance at once.
232 */
233 abstract protected function hydrateAllData(): mixed;
234
235 /**
236 * Processes a row of the result set.
237 *
238 * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
239 * Puts the elements of a result row into a new array, grouped by the dql alias
240 * they belong to. The column names in the result set are mapped to their
241 * field names during this procedure as well as any necessary conversions on
242 * the values applied. Scalar values are kept in a specific key 'scalars'.
243 *
244 * @param mixed[] $data SQL Result Row.
245 * @psalm-param array<string, string> $id Dql-Alias => ID-Hash.
246 * @psalm-param array<string, bool> $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
247 *
248 * @return array<string, array<string, mixed>> An array with all the fields
249 * (name => value) of the data
250 * row, grouped by their
251 * component alias.
252 * @psalm-return array{
253 * data: array<array-key, array>,
254 * newObjects?: array<array-key, array{
255 * class: mixed,
256 * args?: array
257 * }>,
258 * scalars?: array
259 * }
260 */
261 protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
262 {
263 $rowData = ['data' => []];
264
265 foreach ($data as $key => $value) {
266 $cacheKeyInfo = $this->hydrateColumnInfo($key);
267 if ($cacheKeyInfo === null) {
268 continue;
269 }
270
271 $fieldName = $cacheKeyInfo['fieldName'];
272
273 switch (true) {
274 case isset($cacheKeyInfo['isNewObjectParameter']):
275 $argIndex = $cacheKeyInfo['argIndex'];
276 $objIndex = $cacheKeyInfo['objIndex'];
277 $type = $cacheKeyInfo['type'];
278 $value = $type->convertToPHPValue($value, $this->platform);
279
280 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
281 $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
282 }
283
284 $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
285 $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
286 break;
287
288 case isset($cacheKeyInfo['isScalar']):
289 $type = $cacheKeyInfo['type'];
290 $value = $type->convertToPHPValue($value, $this->platform);
291
292 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
293 $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
294 }
295
296 $rowData['scalars'][$fieldName] = $value;
297
298 break;
299
300 //case (isset($cacheKeyInfo['isMetaColumn'])):
301 default:
302 $dqlAlias = $cacheKeyInfo['dqlAlias'];
303 $type = $cacheKeyInfo['type'];
304
305 // If there are field name collisions in the child class, then we need
306 // to only hydrate if we are looking at the correct discriminator value
307 if (
308 isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
309 && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
310 ) {
311 break;
312 }
313
314 // in an inheritance hierarchy the same field could be defined several times.
315 // We overwrite this value so long we don't have a non-null value, that value we keep.
316 // Per definition it cannot be that a field is defined several times and has several values.
317 if (isset($rowData['data'][$dqlAlias][$fieldName])) {
318 break;
319 }
320
321 $rowData['data'][$dqlAlias][$fieldName] = $type
322 ? $type->convertToPHPValue($value, $this->platform)
323 : $value;
324
325 if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) {
326 $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']);
327 }
328
329 if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
330 $id[$dqlAlias] .= '|' . $value;
331 $nonemptyComponents[$dqlAlias] = true;
332 }
333
334 break;
335 }
336 }
337
338 return $rowData;
339 }
340
341 /**
342 * Processes a row of the result set.
343 *
344 * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
345 * simply converts column names to field names and properly converts the
346 * values according to their types. The resulting row has the same number
347 * of elements as before.
348 *
349 * @param mixed[] $data
350 * @psalm-param array<string, mixed> $data
351 *
352 * @return mixed[] The processed row.
353 * @psalm-return array<string, mixed>
354 */
355 protected function gatherScalarRowData(array &$data): array
356 {
357 $rowData = [];
358
359 foreach ($data as $key => $value) {
360 $cacheKeyInfo = $this->hydrateColumnInfo($key);
361 if ($cacheKeyInfo === null) {
362 continue;
363 }
364
365 $fieldName = $cacheKeyInfo['fieldName'];
366
367 // WARNING: BC break! We know this is the desired behavior to type convert values, but this
368 // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
369 if (! isset($cacheKeyInfo['isScalar'])) {
370 $type = $cacheKeyInfo['type'];
371 $value = $type ? $type->convertToPHPValue($value, $this->platform) : $value;
372
373 $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
374 }
375
376 $rowData[$fieldName] = $value;
377 }
378
379 return $rowData;
380 }
381
382 /**
383 * Retrieve column information from ResultSetMapping.
384 *
385 * @param string $key Column name
386 *
387 * @return mixed[]|null
388 * @psalm-return array<string, mixed>|null
389 */
390 protected function hydrateColumnInfo(string $key): array|null
391 {
392 if (isset($this->cache[$key])) {
393 return $this->cache[$key];
394 }
395
396 switch (true) {
397 // NOTE: Most of the times it's a field mapping, so keep it first!!!
398 case isset($this->rsm->fieldMappings[$key]):
399 $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
400 $fieldName = $this->rsm->fieldMappings[$key];
401 $fieldMapping = $classMetadata->fieldMappings[$fieldName];
402 $ownerMap = $this->rsm->columnOwnerMap[$key];
403 $columnInfo = [
404 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true),
405 'fieldName' => $fieldName,
406 'type' => Type::getType($fieldMapping->type),
407 'dqlAlias' => $ownerMap,
408 'enumType' => $this->rsm->enumMappings[$key] ?? null,
409 ];
410
411 // the current discriminator value must be saved in order to disambiguate fields hydration,
412 // should there be field name collisions
413 if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) {
414 return $this->cache[$key] = array_merge(
415 $columnInfo,
416 [
417 'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
418 'discriminatorValue' => $classMetadata->discriminatorValue,
419 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
420 ],
421 );
422 }
423
424 return $this->cache[$key] = $columnInfo;
425
426 case isset($this->rsm->newObjectMappings[$key]):
427 // WARNING: A NEW object is also a scalar, so it must be declared before!
428 $mapping = $this->rsm->newObjectMappings[$key];
429
430 return $this->cache[$key] = [
431 'isScalar' => true,
432 'isNewObjectParameter' => true,
433 'fieldName' => $this->rsm->scalarMappings[$key],
434 'type' => Type::getType($this->rsm->typeMappings[$key]),
435 'argIndex' => $mapping['argIndex'],
436 'objIndex' => $mapping['objIndex'],
437 'class' => new ReflectionClass($mapping['className']),
438 'enumType' => $this->rsm->enumMappings[$key] ?? null,
439 ];
440
441 case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
442 return $this->cache[$key] = [
443 'fieldName' => $this->rsm->scalarMappings[$key],
444 'type' => Type::getType($this->rsm->typeMappings[$key]),
445 'dqlAlias' => '',
446 'enumType' => $this->rsm->enumMappings[$key] ?? null,
447 ];
448
449 case isset($this->rsm->scalarMappings[$key]):
450 return $this->cache[$key] = [
451 'isScalar' => true,
452 'fieldName' => $this->rsm->scalarMappings[$key],
453 'type' => Type::getType($this->rsm->typeMappings[$key]),
454 'enumType' => $this->rsm->enumMappings[$key] ?? null,
455 ];
456
457 case isset($this->rsm->metaMappings[$key]):
458 // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
459 $fieldName = $this->rsm->metaMappings[$key];
460 $dqlAlias = $this->rsm->columnOwnerMap[$key];
461 $type = isset($this->rsm->typeMappings[$key])
462 ? Type::getType($this->rsm->typeMappings[$key])
463 : null;
464
465 // Cache metadata fetch
466 $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
467
468 return $this->cache[$key] = [
469 'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
470 'isMetaColumn' => true,
471 'fieldName' => $fieldName,
472 'type' => $type,
473 'dqlAlias' => $dqlAlias,
474 'enumType' => $this->rsm->enumMappings[$key] ?? null,
475 ];
476 }
477
478 // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
479 // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
480 return null;
481 }
482
483 /**
484 * @return string[]
485 * @psalm-return non-empty-list<string>
486 */
487 private function getDiscriminatorValues(ClassMetadata $classMetadata): array
488 {
489 $values = array_map(
490 fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue,
491 $classMetadata->subClasses,
492 );
493
494 $values[] = (string) $classMetadata->discriminatorValue;
495
496 return $values;
497 }
498
499 /**
500 * Retrieve ClassMetadata associated to entity class name.
501 */
502 protected function getClassMetadata(string $className): ClassMetadata
503 {
504 if (! isset($this->metadataCache[$className])) {
505 $this->metadataCache[$className] = $this->em->getClassMetadata($className);
506 }
507
508 return $this->metadataCache[$className];
509 }
510
511 /**
512 * Register entity as managed in UnitOfWork.
513 *
514 * @param mixed[] $data
515 *
516 * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
517 */
518 protected function registerManaged(ClassMetadata $class, object $entity, array $data): void
519 {
520 if ($class->isIdentifierComposite) {
521 $id = [];
522
523 foreach ($class->identifier as $fieldName) {
524 $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
525 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
526 : $data[$fieldName];
527 }
528 } else {
529 $fieldName = $class->identifier[0];
530 $id = [
531 $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
532 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
533 : $data[$fieldName],
534 ];
535 }
536
537 $this->em->getUnitOfWork()->registerManaged($entity, $id, $data);
538 }
539
540 /**
541 * @param class-string<BackedEnum> $enumType
542 *
543 * @return BackedEnum|array<BackedEnum>
544 */
545 final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array
546 {
547 if (is_array($value)) {
548 return array_map(
549 static fn ($value) => $enumType::from($value),
550 $value,
551 );
552 }
553
554 return $enumType::from($value);
555 }
556}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php
new file mode 100644
index 0000000..7115c16
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php
@@ -0,0 +1,270 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use function array_key_last;
8use function count;
9use function is_array;
10use function key;
11use function reset;
12
13/**
14 * The ArrayHydrator produces a nested array "graph" that is often (not always)
15 * interchangeable with the corresponding object graph for read-only access.
16 */
17class ArrayHydrator extends AbstractHydrator
18{
19 /** @var array<string,bool> */
20 private array $rootAliases = [];
21
22 private bool $isSimpleQuery = false;
23
24 /** @var mixed[] */
25 private array $identifierMap = [];
26
27 /** @var mixed[] */
28 private array $resultPointers = [];
29
30 /** @var array<string,string> */
31 private array $idTemplate = [];
32
33 private int $resultCounter = 0;
34
35 protected function prepare(): void
36 {
37 $this->isSimpleQuery = count($this->resultSetMapping()->aliasMap) <= 1;
38
39 foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) {
40 $this->identifierMap[$dqlAlias] = [];
41 $this->resultPointers[$dqlAlias] = [];
42 $this->idTemplate[$dqlAlias] = '';
43 }
44 }
45
46 /**
47 * {@inheritDoc}
48 */
49 protected function hydrateAllData(): array
50 {
51 $result = [];
52
53 while ($data = $this->statement()->fetchAssociative()) {
54 $this->hydrateRowData($data, $result);
55 }
56
57 return $result;
58 }
59
60 /**
61 * {@inheritDoc}
62 */
63 protected function hydrateRowData(array $row, array &$result): void
64 {
65 // 1) Initialize
66 $id = $this->idTemplate; // initialize the id-memory
67 $nonemptyComponents = [];
68 $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
69
70 // 2) Now hydrate the data found in the current row.
71 foreach ($rowData['data'] as $dqlAlias => $data) {
72 $index = false;
73
74 if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) {
75 // It's a joined result
76
77 $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
78 $path = $parent . '.' . $dqlAlias;
79
80 // missing parent data, skipping as RIGHT JOIN hydration is not supported.
81 if (! isset($nonemptyComponents[$parent])) {
82 continue;
83 }
84
85 // Get a reference to the right element in the result tree.
86 // This element will get the associated element attached.
87 if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parent])) {
88 $first = reset($this->resultPointers);
89 // TODO: Exception if $key === null ?
90 $baseElement =& $this->resultPointers[$parent][key($first)];
91 } elseif (isset($this->resultPointers[$parent])) {
92 $baseElement =& $this->resultPointers[$parent];
93 } else {
94 unset($this->resultPointers[$dqlAlias]); // Ticket #1228
95
96 continue;
97 }
98
99 $relationAlias = $this->resultSetMapping()->relationMap[$dqlAlias];
100 $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parent]];
101 $relation = $parentClass->associationMappings[$relationAlias];
102
103 // Check the type of the relation (many or single-valued)
104 if (! $relation->isToOne()) {
105 $oneToOne = false;
106
107 if (! isset($baseElement[$relationAlias])) {
108 $baseElement[$relationAlias] = [];
109 }
110
111 if (isset($nonemptyComponents[$dqlAlias])) {
112 $indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]);
113 $index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false;
114 $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false;
115
116 if (! $indexExists || ! $indexIsValid) {
117 $element = $data;
118
119 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
120 $baseElement[$relationAlias][$row[$this->resultSetMapping()->indexByMap[$dqlAlias]]] = $element;
121 } else {
122 $baseElement[$relationAlias][] = $element;
123 }
124
125 $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = array_key_last($baseElement[$relationAlias]);
126 }
127 }
128 } else {
129 $oneToOne = true;
130
131 if (
132 ! isset($nonemptyComponents[$dqlAlias]) &&
133 ( ! isset($baseElement[$relationAlias]))
134 ) {
135 $baseElement[$relationAlias] = null;
136 } elseif (! isset($baseElement[$relationAlias])) {
137 $baseElement[$relationAlias] = $data;
138 }
139 }
140
141 $coll =& $baseElement[$relationAlias];
142
143 if (is_array($coll)) {
144 $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne);
145 }
146 } else {
147 // It's a root result element
148
149 $this->rootAliases[$dqlAlias] = true; // Mark as root
150 $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0;
151
152 // if this row has a NULL value for the root result id then make it a null result.
153 if (! isset($nonemptyComponents[$dqlAlias])) {
154 $result[] = $this->resultSetMapping()->isMixed
155 ? [$entityKey => null]
156 : null;
157
158 $resultKey = $this->resultCounter;
159 ++$this->resultCounter;
160
161 continue;
162 }
163
164 // Check for an existing element
165 if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
166 $element = $this->resultSetMapping()->isMixed
167 ? [$entityKey => $data]
168 : $data;
169
170 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
171 $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
172 $result[$resultKey] = $element;
173 } else {
174 $resultKey = $this->resultCounter;
175 $result[] = $element;
176
177 ++$this->resultCounter;
178 }
179
180 $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
181 } else {
182 $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
183 $resultKey = $index;
184 }
185
186 $this->updateResultPointer($result, $index, $dqlAlias, false);
187 }
188 }
189
190 if (! isset($resultKey)) {
191 $this->resultCounter++;
192 }
193
194 // Append scalar values to mixed result sets
195 if (isset($rowData['scalars'])) {
196 if (! isset($resultKey)) {
197 // this only ever happens when no object is fetched (scalar result only)
198 $resultKey = isset($this->resultSetMapping()->indexByMap['scalars'])
199 ? $row[$this->resultSetMapping()->indexByMap['scalars']]
200 : $this->resultCounter - 1;
201 }
202
203 foreach ($rowData['scalars'] as $name => $value) {
204 $result[$resultKey][$name] = $value;
205 }
206 }
207
208 // Append new object to mixed result sets
209 if (isset($rowData['newObjects'])) {
210 if (! isset($resultKey)) {
211 $resultKey = $this->resultCounter - 1;
212 }
213
214 $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
215
216 foreach ($rowData['newObjects'] as $objIndex => $newObject) {
217 $class = $newObject['class'];
218 $args = $newObject['args'];
219 $obj = $class->newInstanceArgs($args);
220
221 if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
222 $result[$resultKey] = $obj;
223
224 continue;
225 }
226
227 $result[$resultKey][$objIndex] = $obj;
228 }
229 }
230 }
231
232 /**
233 * Updates the result pointer for an Entity. The result pointers point to the
234 * last seen instance of each Entity type. This is used for graph construction.
235 *
236 * @param mixed[]|null $coll The element.
237 * @param string|int|false $index Index of the element in the collection.
238 * @param bool $oneToOne Whether it is a single-valued association or not.
239 */
240 private function updateResultPointer(
241 array|null &$coll,
242 string|int|false $index,
243 string $dqlAlias,
244 bool $oneToOne,
245 ): void {
246 if ($coll === null) {
247 unset($this->resultPointers[$dqlAlias]); // Ticket #1228
248
249 return;
250 }
251
252 if ($oneToOne) {
253 $this->resultPointers[$dqlAlias] =& $coll;
254
255 return;
256 }
257
258 if ($index !== false) {
259 $this->resultPointers[$dqlAlias] =& $coll[$index];
260
261 return;
262 }
263
264 if (! $coll) {
265 return;
266 }
267
268 $this->resultPointers[$dqlAlias] =& $coll[array_key_last($coll)];
269 }
270}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php
new file mode 100644
index 0000000..710114f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php
@@ -0,0 +1,67 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\Exception\ORMException;
8use Exception;
9
10use function implode;
11use function sprintf;
12
13class HydrationException extends Exception implements ORMException
14{
15 public static function nonUniqueResult(): self
16 {
17 return new self('The result returned by the query was not unique.');
18 }
19
20 public static function parentObjectOfRelationNotFound(string $alias, string $parentAlias): self
21 {
22 return new self(sprintf(
23 "The parent object of entity result with alias '%s' was not found."
24 . " The parent alias is '%s'.",
25 $alias,
26 $parentAlias,
27 ));
28 }
29
30 public static function emptyDiscriminatorValue(string $dqlAlias): self
31 {
32 return new self("The DQL alias '" . $dqlAlias . "' contains an entity " .
33 'of an inheritance hierarchy with an empty discriminator value. This means ' .
34 'that the database contains inconsistent data with an empty ' .
35 'discriminator value in a table row.');
36 }
37
38 public static function missingDiscriminatorColumn(string $entityName, string $discrColumnName, string $dqlAlias): self
39 {
40 return new self(sprintf(
41 'The discriminator column "%s" is missing for "%s" using the DQL alias "%s".',
42 $discrColumnName,
43 $entityName,
44 $dqlAlias,
45 ));
46 }
47
48 public static function missingDiscriminatorMetaMappingColumn(string $entityName, string $discrColumnName, string $dqlAlias): self
49 {
50 return new self(sprintf(
51 'The meta mapping for the discriminator column "%s" is missing for "%s" using the DQL alias "%s".',
52 $discrColumnName,
53 $entityName,
54 $dqlAlias,
55 ));
56 }
57
58 /** @param list<int|string> $discrValues */
59 public static function invalidDiscriminatorValue(string $discrValue, array $discrValues): self
60 {
61 return new self(sprintf(
62 'The discriminator value "%s" is invalid. It must be one of "%s".',
63 $discrValue,
64 implode('", "', $discrValues),
65 ));
66 }
67}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php
new file mode 100644
index 0000000..d0fc101
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php
@@ -0,0 +1,586 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use BackedEnum;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\ORM\Mapping\ClassMetadata;
10use Doctrine\ORM\PersistentCollection;
11use Doctrine\ORM\Query;
12use Doctrine\ORM\UnitOfWork;
13
14use function array_fill_keys;
15use function array_keys;
16use function array_map;
17use function assert;
18use function count;
19use function is_array;
20use function key;
21use function ltrim;
22use function spl_object_id;
23
24/**
25 * The ObjectHydrator constructs an object graph out of an SQL result set.
26 *
27 * Internal note: Highly performance-sensitive code.
28 */
29class ObjectHydrator extends AbstractHydrator
30{
31 /** @var mixed[] */
32 private array $identifierMap = [];
33
34 /** @var mixed[] */
35 private array $resultPointers = [];
36
37 /** @var mixed[] */
38 private array $idTemplate = [];
39
40 private int $resultCounter = 0;
41
42 /** @var mixed[] */
43 private array $rootAliases = [];
44
45 /** @var mixed[] */
46 private array $initializedCollections = [];
47
48 /** @var array<string, PersistentCollection> */
49 private array $uninitializedCollections = [];
50
51 /** @var mixed[] */
52 private array $existingCollections = [];
53
54 protected function prepare(): void
55 {
56 if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) {
57 $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true;
58 }
59
60 foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) {
61 $this->identifierMap[$dqlAlias] = [];
62 $this->idTemplate[$dqlAlias] = '';
63
64 // Remember which associations are "fetch joined", so that we know where to inject
65 // collection stubs or proxies and where not.
66 if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) {
67 continue;
68 }
69
70 $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
71
72 if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
73 throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
74 }
75
76 $sourceClassName = $this->resultSetMapping()->aliasMap[$parent];
77 $sourceClass = $this->getClassMetadata($sourceClassName);
78 $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]];
79
80 $this->hints['fetched'][$parent][$assoc->fieldName] = true;
81
82 if ($assoc->isManyToMany()) {
83 continue;
84 }
85
86 // Mark any non-collection opposite sides as fetched, too.
87 if (! $assoc->isOwningSide()) {
88 $this->hints['fetched'][$dqlAlias][$assoc->mappedBy] = true;
89
90 continue;
91 }
92
93 // handle fetch-joined owning side bi-directional one-to-one associations
94 if ($assoc->inversedBy !== null) {
95 $class = $this->getClassMetadata($className);
96 $inverseAssoc = $class->associationMappings[$assoc->inversedBy];
97
98 if (! $inverseAssoc->isToOne()) {
99 continue;
100 }
101
102 $this->hints['fetched'][$dqlAlias][$inverseAssoc->fieldName] = true;
103 }
104 }
105 }
106
107 protected function cleanup(): void
108 {
109 $eagerLoad = isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true;
110
111 parent::cleanup();
112
113 $this->identifierMap =
114 $this->initializedCollections =
115 $this->uninitializedCollections =
116 $this->existingCollections =
117 $this->resultPointers = [];
118
119 if ($eagerLoad) {
120 $this->uow->triggerEagerLoads();
121 }
122
123 $this->uow->hydrationComplete();
124 }
125
126 protected function cleanupAfterRowIteration(): void
127 {
128 $this->identifierMap =
129 $this->initializedCollections =
130 $this->uninitializedCollections =
131 $this->existingCollections =
132 $this->resultPointers = [];
133 }
134
135 /**
136 * {@inheritDoc}
137 */
138 protected function hydrateAllData(): array
139 {
140 $result = [];
141
142 while ($row = $this->statement()->fetchAssociative()) {
143 $this->hydrateRowData($row, $result);
144 }
145
146 // Take snapshots from all newly initialized collections
147 foreach ($this->initializedCollections as $coll) {
148 $coll->takeSnapshot();
149 }
150
151 foreach ($this->uninitializedCollections as $coll) {
152 if (! $coll->isInitialized()) {
153 $coll->setInitialized(true);
154 }
155 }
156
157 return $result;
158 }
159
160 /**
161 * Initializes a related collection.
162 *
163 * @param string $fieldName The name of the field on the entity that holds the collection.
164 * @param string $parentDqlAlias Alias of the parent fetch joining this collection.
165 */
166 private function initRelatedCollection(
167 object $entity,
168 ClassMetadata $class,
169 string $fieldName,
170 string $parentDqlAlias,
171 ): PersistentCollection {
172 $oid = spl_object_id($entity);
173 $relation = $class->associationMappings[$fieldName];
174 $value = $class->reflFields[$fieldName]->getValue($entity);
175
176 if ($value === null || is_array($value)) {
177 $value = new ArrayCollection((array) $value);
178 }
179
180 if (! $value instanceof PersistentCollection) {
181 assert($relation->isToMany());
182 $value = new PersistentCollection(
183 $this->em,
184 $this->metadataCache[$relation->targetEntity],
185 $value,
186 );
187 $value->setOwner($entity, $relation);
188
189 $class->reflFields[$fieldName]->setValue($entity, $value);
190 $this->uow->setOriginalEntityProperty($oid, $fieldName, $value);
191
192 $this->initializedCollections[$oid . $fieldName] = $value;
193 } elseif (
194 isset($this->hints[Query::HINT_REFRESH]) ||
195 isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) &&
196 ! $value->isInitialized()
197 ) {
198 // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
199 $value->setDirty(false);
200 $value->setInitialized(true);
201 $value->unwrap()->clear();
202
203 $this->initializedCollections[$oid . $fieldName] = $value;
204 } else {
205 // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
206 $this->existingCollections[$oid . $fieldName] = $value;
207 }
208
209 return $value;
210 }
211
212 /**
213 * Gets an entity instance.
214 *
215 * @param string $dqlAlias The DQL alias of the entity's class.
216 * @psalm-param array<string, mixed> $data The instance data.
217 *
218 * @throws HydrationException
219 */
220 private function getEntity(array $data, string $dqlAlias): object
221 {
222 $className = $this->resultSetMapping()->aliasMap[$dqlAlias];
223
224 if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) {
225 $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias];
226
227 if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) {
228 throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias);
229 }
230
231 $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName];
232
233 if (! isset($data[$discrColumn])) {
234 throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias);
235 }
236
237 if ($data[$discrColumn] === '') {
238 throw HydrationException::emptyDiscriminatorValue($dqlAlias);
239 }
240
241 $discrMap = $this->metadataCache[$className]->discriminatorMap;
242 $discriminatorValue = $data[$discrColumn];
243 if ($discriminatorValue instanceof BackedEnum) {
244 $discriminatorValue = $discriminatorValue->value;
245 }
246
247 $discriminatorValue = (string) $discriminatorValue;
248
249 if (! isset($discrMap[$discriminatorValue])) {
250 throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap));
251 }
252
253 $className = $discrMap[$discriminatorValue];
254
255 unset($data[$discrColumn]);
256 }
257
258 if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) {
259 $this->registerManaged($this->metadataCache[$className], $this->hints[Query::HINT_REFRESH_ENTITY], $data);
260 }
261
262 $this->hints['fetchAlias'] = $dqlAlias;
263
264 return $this->uow->createEntity($className, $data, $this->hints);
265 }
266
267 /**
268 * @psalm-param class-string $className
269 * @psalm-param array<string, mixed> $data
270 */
271 private function getEntityFromIdentityMap(string $className, array $data): object|bool
272 {
273 // TODO: Abstract this code and UnitOfWork::createEntity() equivalent?
274 $class = $this->metadataCache[$className];
275
276 if ($class->isIdentifierComposite) {
277 $idHash = UnitOfWork::getIdHashByIdentifier(
278 array_map(
279 /** @return mixed */
280 static fn (string $fieldName) => isset($class->associationMappings[$fieldName]) && assert($class->associationMappings[$fieldName]->isToOneOwningSide())
281 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
282 : $data[$fieldName],
283 $class->identifier,
284 ),
285 );
286
287 return $this->uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName);
288 } elseif (isset($class->associationMappings[$class->identifier[0]])) {
289 $association = $class->associationMappings[$class->identifier[0]];
290 assert($association->isToOneOwningSide());
291
292 return $this->uow->tryGetByIdHash($data[$association->joinColumns[0]->name], $class->rootEntityName);
293 }
294
295 return $this->uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName);
296 }
297
298 /**
299 * Hydrates a single row in an SQL result set.
300 *
301 * @internal
302 * First, the data of the row is split into chunks where each chunk contains data
303 * that belongs to a particular component/class. Afterwards, all these chunks
304 * are processed, one after the other. For each chunk of class data only one of the
305 * following code paths is executed:
306 * Path A: The data chunk belongs to a joined/associated object and the association
307 * is collection-valued.
308 * Path B: The data chunk belongs to a joined/associated object and the association
309 * is single-valued.
310 * Path C: The data chunk belongs to a root result element/object that appears in the topmost
311 * level of the hydrated result. A typical example are the objects of the type
312 * specified by the FROM clause in a DQL query.
313 *
314 * @param mixed[] $row The data of the row to process.
315 * @param mixed[] $result The result array to fill.
316 */
317 protected function hydrateRowData(array $row, array &$result): void
318 {
319 // Initialize
320 $id = $this->idTemplate; // initialize the id-memory
321 $nonemptyComponents = [];
322 // Split the row data into chunks of class data.
323 $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
324
325 // reset result pointers for each data row
326 $this->resultPointers = [];
327
328 // Hydrate the data chunks
329 foreach ($rowData['data'] as $dqlAlias => $data) {
330 $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias];
331
332 if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) {
333 // It's a joined result
334
335 $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
336 // we need the $path to save into the identifier map which entities were already
337 // seen for this parent-child relationship
338 $path = $parentAlias . '.' . $dqlAlias;
339
340 // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs
341 if (! isset($nonemptyComponents[$parentAlias])) {
342 // TODO: Add special case code where we hydrate the right join objects into identity map at least
343 continue;
344 }
345
346 $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]];
347 $relationField = $this->resultSetMapping()->relationMap[$dqlAlias];
348 $relation = $parentClass->associationMappings[$relationField];
349 $reflField = $parentClass->reflFields[$relationField];
350
351 // Get a reference to the parent object to which the joined element belongs.
352 if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) {
353 $objectClass = $this->resultPointers[$parentAlias];
354 $parentObject = $objectClass[key($objectClass)];
355 } elseif (isset($this->resultPointers[$parentAlias])) {
356 $parentObject = $this->resultPointers[$parentAlias];
357 } else {
358 // Parent object of relation not found, mark as not-fetched again
359 if (isset($nonemptyComponents[$dqlAlias])) {
360 $element = $this->getEntity($data, $dqlAlias);
361
362 // Update result pointer and provide initial fetch data for parent
363 $this->resultPointers[$dqlAlias] = $element;
364 $rowData['data'][$parentAlias][$relationField] = $element;
365 } else {
366 $element = null;
367 }
368
369 // Mark as not-fetched again
370 unset($this->hints['fetched'][$parentAlias][$relationField]);
371 continue;
372 }
373
374 $oid = spl_object_id($parentObject);
375
376 // Check the type of the relation (many or single-valued)
377 if (! $relation->isToOne()) {
378 // PATH A: Collection-valued association
379 $reflFieldValue = $reflField->getValue($parentObject);
380
381 if (isset($nonemptyComponents[$dqlAlias])) {
382 $collKey = $oid . $relationField;
383 if (isset($this->initializedCollections[$collKey])) {
384 $reflFieldValue = $this->initializedCollections[$collKey];
385 } elseif (! isset($this->existingCollections[$collKey])) {
386 $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
387 }
388
389 $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
390 $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
391 $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
392
393 if (! $indexExists || ! $indexIsValid) {
394 if (isset($this->existingCollections[$collKey])) {
395 // Collection exists, only look for the element in the identity map.
396 $element = $this->getEntityFromIdentityMap($entityName, $data);
397 if ($element) {
398 $this->resultPointers[$dqlAlias] = $element;
399 } else {
400 unset($this->resultPointers[$dqlAlias]);
401 }
402 } else {
403 $element = $this->getEntity($data, $dqlAlias);
404
405 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
406 $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
407 $reflFieldValue->hydrateSet($indexValue, $element);
408 $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
409 } else {
410 if (! $reflFieldValue->contains($element)) {
411 $reflFieldValue->hydrateAdd($element);
412 $reflFieldValue->last();
413 }
414
415 $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
416 }
417
418 // Update result pointer
419 $this->resultPointers[$dqlAlias] = $element;
420 }
421 } else {
422 // Update result pointer
423 $this->resultPointers[$dqlAlias] = $reflFieldValue[$index];
424 }
425 } elseif (! $reflFieldValue) {
426 $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
427 } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) {
428 $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue;
429 }
430 } else {
431 // PATH B: Single-valued association
432 $reflFieldValue = $reflField->getValue($parentObject);
433
434 if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) || $this->uow->isUninitializedObject($reflFieldValue)) {
435 // we only need to take action if this value is null,
436 // we refresh the entity or its an uninitialized proxy.
437 if (isset($nonemptyComponents[$dqlAlias])) {
438 $element = $this->getEntity($data, $dqlAlias);
439 $reflField->setValue($parentObject, $element);
440 $this->uow->setOriginalEntityProperty($oid, $relationField, $element);
441 $targetClass = $this->metadataCache[$relation->targetEntity];
442
443 if ($relation->isOwningSide()) {
444 // TODO: Just check hints['fetched'] here?
445 // If there is an inverse mapping on the target class its bidirectional
446 if ($relation->inversedBy !== null) {
447 $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
448 if ($inverseAssoc->isToOne()) {
449 $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);
450 $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject);
451 }
452 }
453 } else {
454 // For sure bidirectional, as there is no inverse side in unidirectional mappings
455 $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject);
456 $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject);
457 }
458
459 // Update result pointer
460 $this->resultPointers[$dqlAlias] = $element;
461 } else {
462 $this->uow->setOriginalEntityProperty($oid, $relationField, null);
463 $reflField->setValue($parentObject, null);
464 }
465 // else leave $reflFieldValue null for single-valued associations
466 } else {
467 // Update result pointer
468 $this->resultPointers[$dqlAlias] = $reflFieldValue;
469 }
470 }
471 } else {
472 // PATH C: Its a root result element
473 $this->rootAliases[$dqlAlias] = true; // Mark as root alias
474 $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0;
475
476 // if this row has a NULL value for the root result id then make it a null result.
477 if (! isset($nonemptyComponents[$dqlAlias])) {
478 if ($this->resultSetMapping()->isMixed) {
479 $result[] = [$entityKey => null];
480 } else {
481 $result[] = null;
482 }
483
484 $resultKey = $this->resultCounter;
485 ++$this->resultCounter;
486 continue;
487 }
488
489 // check for existing result from the iterations before
490 if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
491 $element = $this->getEntity($data, $dqlAlias);
492
493 if ($this->resultSetMapping()->isMixed) {
494 $element = [$entityKey => $element];
495 }
496
497 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
498 $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
499
500 if (isset($this->hints['collection'])) {
501 $this->hints['collection']->hydrateSet($resultKey, $element);
502 }
503
504 $result[$resultKey] = $element;
505 } else {
506 $resultKey = $this->resultCounter;
507 ++$this->resultCounter;
508
509 if (isset($this->hints['collection'])) {
510 $this->hints['collection']->hydrateAdd($element);
511 }
512
513 $result[] = $element;
514 }
515
516 $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
517
518 // Update result pointer
519 $this->resultPointers[$dqlAlias] = $element;
520 } else {
521 // Update result pointer
522 $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
523 $this->resultPointers[$dqlAlias] = $result[$index];
524 $resultKey = $index;
525 }
526 }
527
528 if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) {
529 $this->uow->hydrationComplete();
530 }
531 }
532
533 if (! isset($resultKey)) {
534 $this->resultCounter++;
535 }
536
537 // Append scalar values to mixed result sets
538 if (isset($rowData['scalars'])) {
539 if (! isset($resultKey)) {
540 $resultKey = isset($this->resultSetMapping()->indexByMap['scalars'])
541 ? $row[$this->resultSetMapping()->indexByMap['scalars']]
542 : $this->resultCounter - 1;
543 }
544
545 foreach ($rowData['scalars'] as $name => $value) {
546 $result[$resultKey][$name] = $value;
547 }
548 }
549
550 // Append new object to mixed result sets
551 if (isset($rowData['newObjects'])) {
552 if (! isset($resultKey)) {
553 $resultKey = $this->resultCounter - 1;
554 }
555
556 $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
557
558 foreach ($rowData['newObjects'] as $objIndex => $newObject) {
559 $class = $newObject['class'];
560 $args = $newObject['args'];
561 $obj = $class->newInstanceArgs($args);
562
563 if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
564 $result[$resultKey] = $obj;
565
566 continue;
567 }
568
569 $result[$resultKey][$objIndex] = $obj;
570 }
571 }
572 }
573
574 /**
575 * When executed in a hydrate() loop we may have to clear internal state to
576 * decrease memory consumption.
577 */
578 public function onClear(mixed $eventArgs): void
579 {
580 parent::onClear($eventArgs);
581
582 $aliases = array_keys($this->identifierMap);
583
584 $this->identifierMap = array_fill_keys($aliases, []);
585 }
586}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php
new file mode 100644
index 0000000..0f10fb4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\DBAL\Driver\Exception;
8use Doctrine\ORM\Exception\MultipleSelectorsFoundException;
9
10use function array_column;
11use function count;
12
13/**
14 * Hydrator that produces one-dimensional array.
15 */
16final class ScalarColumnHydrator extends AbstractHydrator
17{
18 /**
19 * {@inheritDoc}
20 *
21 * @throws MultipleSelectorsFoundException
22 * @throws Exception
23 */
24 protected function hydrateAllData(): array
25 {
26 if (count($this->resultSetMapping()->fieldMappings) > 1) {
27 throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings);
28 }
29
30 $result = $this->statement()->fetchAllNumeric();
31
32 return array_column($result, 0);
33 }
34}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php
new file mode 100644
index 0000000..15f3e7e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7/**
8 * Hydrator that produces flat, rectangular results of scalar data.
9 * The created result is almost the same as a regular SQL result set, except
10 * that column names are mapped to field names and data type conversions take place.
11 */
12class ScalarHydrator extends AbstractHydrator
13{
14 /**
15 * {@inheritDoc}
16 */
17 protected function hydrateAllData(): array
18 {
19 $result = [];
20
21 while ($data = $this->statement()->fetchAssociative()) {
22 $this->hydrateRowData($data, $result);
23 }
24
25 return $result;
26 }
27
28 /**
29 * {@inheritDoc}
30 */
31 protected function hydrateRowData(array $row, array &$result): void
32 {
33 $result[] = $this->gatherScalarRowData($row);
34 }
35}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php
new file mode 100644
index 0000000..eab7b9b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php
@@ -0,0 +1,176 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\Internal\SQLResultCasing;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\Mapping\MappingException;
10use Doctrine\ORM\Query;
11use Exception;
12use RuntimeException;
13use ValueError;
14
15use function array_keys;
16use function array_search;
17use function assert;
18use function count;
19use function in_array;
20use function key;
21use function reset;
22use function sprintf;
23
24class SimpleObjectHydrator extends AbstractHydrator
25{
26 use SQLResultCasing;
27
28 private ClassMetadata|null $class = null;
29
30 protected function prepare(): void
31 {
32 if (count($this->resultSetMapping()->aliasMap) !== 1) {
33 throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result.');
34 }
35
36 if ($this->resultSetMapping()->scalarMappings) {
37 throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings.');
38 }
39
40 $this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap));
41 }
42
43 protected function cleanup(): void
44 {
45 parent::cleanup();
46
47 $this->uow->triggerEagerLoads();
48 $this->uow->hydrationComplete();
49 }
50
51 /**
52 * {@inheritDoc}
53 */
54 protected function hydrateAllData(): array
55 {
56 $result = [];
57
58 while ($row = $this->statement()->fetchAssociative()) {
59 $this->hydrateRowData($row, $result);
60 }
61
62 $this->em->getUnitOfWork()->triggerEagerLoads();
63
64 return $result;
65 }
66
67 /**
68 * {@inheritDoc}
69 */
70 protected function hydrateRowData(array $row, array &$result): void
71 {
72 assert($this->class !== null);
73 $entityName = $this->class->name;
74 $data = [];
75 $discrColumnValue = null;
76
77 // We need to find the correct entity class name if we have inheritance in resultset
78 if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
79 $discrColumn = $this->class->getDiscriminatorColumn();
80 $discrColumnName = $this->getSQLResultCasing($this->platform, $discrColumn->name);
81
82 // Find mapped discriminator column from the result set.
83 $metaMappingDiscrColumnName = array_search($discrColumnName, $this->resultSetMapping()->metaMappings, true);
84 if ($metaMappingDiscrColumnName) {
85 $discrColumnName = $metaMappingDiscrColumnName;
86 }
87
88 if (! isset($row[$discrColumnName])) {
89 throw HydrationException::missingDiscriminatorColumn(
90 $entityName,
91 $discrColumnName,
92 key($this->resultSetMapping()->aliasMap),
93 );
94 }
95
96 if ($row[$discrColumnName] === '') {
97 throw HydrationException::emptyDiscriminatorValue(key(
98 $this->resultSetMapping()->aliasMap,
99 ));
100 }
101
102 $discrMap = $this->class->discriminatorMap;
103
104 if (! isset($discrMap[$row[$discrColumnName]])) {
105 throw HydrationException::invalidDiscriminatorValue($row[$discrColumnName], array_keys($discrMap));
106 }
107
108 $entityName = $discrMap[$row[$discrColumnName]];
109 $discrColumnValue = $row[$discrColumnName];
110
111 unset($row[$discrColumnName]);
112 }
113
114 foreach ($row as $column => $value) {
115 // An ObjectHydrator should be used instead of SimpleObjectHydrator
116 if (isset($this->resultSetMapping()->relationMap[$column])) {
117 throw new Exception(sprintf('Unable to retrieve association information for column "%s"', $column));
118 }
119
120 $cacheKeyInfo = $this->hydrateColumnInfo($column);
121
122 if (! $cacheKeyInfo) {
123 continue;
124 }
125
126 // If we have inheritance in resultset, make sure the field belongs to the correct class
127 if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) {
128 continue;
129 }
130
131 // Check if value is null before conversion (because some types convert null to something else)
132 $valueIsNull = $value === null;
133
134 // Convert field to a valid PHP value
135 if (isset($cacheKeyInfo['type'])) {
136 $type = $cacheKeyInfo['type'];
137 $value = $type->convertToPHPValue($value, $this->platform);
138 }
139
140 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
141 $originalValue = $value;
142 try {
143 $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
144 } catch (ValueError $e) {
145 throw MappingException::invalidEnumValue(
146 $entityName,
147 $cacheKeyInfo['fieldName'],
148 (string) $originalValue,
149 $cacheKeyInfo['enumType'],
150 $e,
151 );
152 }
153 }
154
155 $fieldName = $cacheKeyInfo['fieldName'];
156
157 // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator)
158 if (! isset($data[$fieldName]) || ! $valueIsNull) {
159 $data[$fieldName] = $value;
160 }
161 }
162
163 if (isset($this->hints[Query::HINT_REFRESH_ENTITY])) {
164 $this->registerManaged($this->class, $this->hints[Query::HINT_REFRESH_ENTITY], $data);
165 }
166
167 $uow = $this->em->getUnitOfWork();
168 $entity = $uow->createEntity($entityName, $data, $this->hints);
169
170 $result[] = $entity;
171
172 if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) {
173 $this->uow->hydrationComplete();
174 }
175 }
176}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php
new file mode 100644
index 0000000..2787bbc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\NonUniqueResultException;
8use Doctrine\ORM\NoResultException;
9
10use function array_shift;
11use function count;
12use function key;
13
14/**
15 * Hydrator that hydrates a single scalar value from the result set.
16 */
17class SingleScalarHydrator extends AbstractHydrator
18{
19 protected function hydrateAllData(): mixed
20 {
21 $data = $this->statement()->fetchAllAssociative();
22 $numRows = count($data);
23
24 if ($numRows === 0) {
25 throw new NoResultException();
26 }
27
28 if ($numRows > 1) {
29 throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().');
30 }
31
32 $result = $this->gatherScalarRowData($data[key($data)]);
33
34 if (count($result) > 1) {
35 throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().');
36 }
37
38 return array_shift($result);
39 }
40}
diff --git a/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php
new file mode 100644
index 0000000..e0fe342
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php
@@ -0,0 +1,64 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Event\ListenersInvoker;
9use Doctrine\ORM\Event\PostLoadEventArgs;
10use Doctrine\ORM\Events;
11use Doctrine\ORM\Mapping\ClassMetadata;
12
13/**
14 * Class, which can handle completion of hydration cycle and produce some of tasks.
15 * In current implementation triggers deferred postLoad event.
16 */
17final class HydrationCompleteHandler
18{
19 /** @var mixed[][] */
20 private array $deferredPostLoadInvocations = [];
21
22 public function __construct(
23 private readonly ListenersInvoker $listenersInvoker,
24 private readonly EntityManagerInterface $em,
25 ) {
26 }
27
28 /**
29 * Method schedules invoking of postLoad entity to the very end of current hydration cycle.
30 */
31 public function deferPostLoadInvoking(ClassMetadata $class, object $entity): void
32 {
33 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad);
34
35 if ($invoke === ListenersInvoker::INVOKE_NONE) {
36 return;
37 }
38
39 $this->deferredPostLoadInvocations[] = [$class, $invoke, $entity];
40 }
41
42 /**
43 * This method should be called after any hydration cycle completed.
44 *
45 * Method fires all deferred invocations of postLoad events
46 */
47 public function hydrationComplete(): void
48 {
49 $toInvoke = $this->deferredPostLoadInvocations;
50 $this->deferredPostLoadInvocations = [];
51
52 foreach ($toInvoke as $classAndEntity) {
53 [$class, $invoke, $entity] = $classAndEntity;
54
55 $this->listenersInvoker->invoke(
56 $class,
57 Events::postLoad,
58 $entity,
59 new PostLoadEventArgs($entity, $this->em),
60 $invoke,
61 );
62 }
63 }
64}
diff --git a/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php
new file mode 100644
index 0000000..7584744
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php
@@ -0,0 +1,55 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use BadMethodCallException;
8
9use function array_filter;
10use function array_is_list;
11use function array_keys;
12use function array_values;
13use function assert;
14use function debug_backtrace;
15use function implode;
16use function is_string;
17use function sprintf;
18
19use const DEBUG_BACKTRACE_IGNORE_ARGS;
20
21/**
22 * Checks if a variadic parameter contains unexpected named arguments.
23 *
24 * @internal
25 */
26trait NoUnknownNamedArguments
27{
28 /**
29 * @param TItem[] $parameter
30 *
31 * @template TItem
32 * @psalm-assert list<TItem> $parameter
33 */
34 private static function validateVariadicParameter(array $parameter): void
35 {
36 if (array_is_list($parameter)) {
37 return;
38 }
39
40 [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
41 assert(isset($trace['class']));
42
43 $additionalArguments = array_values(array_filter(
44 array_keys($parameter),
45 is_string(...),
46 ));
47
48 throw new BadMethodCallException(sprintf(
49 'Invalid call to %s::%s(), unknown named arguments: %s',
50 $trace['class'],
51 $trace['function'],
52 implode(', ', $additionalArguments),
53 ));
54 }
55}
diff --git a/vendor/doctrine/orm/src/Internal/QueryType.php b/vendor/doctrine/orm/src/Internal/QueryType.php
new file mode 100644
index 0000000..b5e60c7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/QueryType.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7/** @internal To be used inside the QueryBuilder only. */
8enum QueryType
9{
10 case Select;
11 case Delete;
12 case Update;
13}
diff --git a/vendor/doctrine/orm/src/Internal/SQLResultCasing.php b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php
new file mode 100644
index 0000000..53b412e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php
@@ -0,0 +1,30 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\DB2Platform;
9use Doctrine\DBAL\Platforms\OraclePlatform;
10use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
11
12use function strtolower;
13use function strtoupper;
14
15/** @internal */
16trait SQLResultCasing
17{
18 private function getSQLResultCasing(AbstractPlatform $platform, string $column): string
19 {
20 if ($platform instanceof DB2Platform || $platform instanceof OraclePlatform) {
21 return strtoupper($column);
22 }
23
24 if ($platform instanceof PostgreSQLPlatform) {
25 return strtolower($column);
26 }
27
28 return $column;
29 }
30}
diff --git a/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php
new file mode 100644
index 0000000..dd4fc98
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php
@@ -0,0 +1,159 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use InvalidArgumentException;
8
9use function array_keys;
10use function array_pop;
11use function array_push;
12use function min;
13use function spl_object_id;
14
15/**
16 * StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
17 * components (SCC) in a directed graph. This algorithm has a linear running time based on
18 * nodes (V) and edges between the nodes (E), resulting in a computational complexity
19 * of O(V + E).
20 *
21 * See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
22 * for an explanation and the meaning of the DFS and lowlink numbers.
23 *
24 * @internal
25 */
26final class StronglyConnectedComponents
27{
28 private const NOT_VISITED = 1;
29 private const IN_PROGRESS = 2;
30 private const VISITED = 3;
31
32 /**
33 * Array of all nodes, indexed by object ids.
34 *
35 * @var array<int, object>
36 */
37 private array $nodes = [];
38
39 /**
40 * DFS state for the different nodes, indexed by node object id and using one of
41 * this class' constants as value.
42 *
43 * @var array<int, self::*>
44 */
45 private array $states = [];
46
47 /**
48 * Edges between the nodes. The first-level key is the object id of the outgoing
49 * node; the second array maps the destination node by object id as key.
50 *
51 * @var array<int, array<int, bool>>
52 */
53 private array $edges = [];
54
55 /**
56 * DFS numbers, by object ID
57 *
58 * @var array<int, int>
59 */
60 private array $dfs = [];
61
62 /**
63 * lowlink numbers, by object ID
64 *
65 * @var array<int, int>
66 */
67 private array $lowlink = [];
68
69 private int $maxdfs = 0;
70
71 /**
72 * Nodes representing the SCC another node is in, indexed by lookup-node object ID
73 *
74 * @var array<int, object>
75 */
76 private array $representingNodes = [];
77
78 /**
79 * Stack with OIDs of nodes visited in the current state of the DFS
80 *
81 * @var list<int>
82 */
83 private array $stack = [];
84
85 public function addNode(object $node): void
86 {
87 $id = spl_object_id($node);
88 $this->nodes[$id] = $node;
89 $this->states[$id] = self::NOT_VISITED;
90 $this->edges[$id] = [];
91 }
92
93 public function hasNode(object $node): bool
94 {
95 return isset($this->nodes[spl_object_id($node)]);
96 }
97
98 /**
99 * Adds a new edge between two nodes to the graph
100 */
101 public function addEdge(object $from, object $to): void
102 {
103 $fromId = spl_object_id($from);
104 $toId = spl_object_id($to);
105
106 $this->edges[$fromId][$toId] = true;
107 }
108
109 public function findStronglyConnectedComponents(): void
110 {
111 foreach (array_keys($this->nodes) as $oid) {
112 if ($this->states[$oid] === self::NOT_VISITED) {
113 $this->tarjan($oid);
114 }
115 }
116 }
117
118 private function tarjan(int $oid): void
119 {
120 $this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
121 $this->states[$oid] = self::IN_PROGRESS;
122 array_push($this->stack, $oid);
123
124 foreach ($this->edges[$oid] as $adjacentId => $ignored) {
125 if ($this->states[$adjacentId] === self::NOT_VISITED) {
126 $this->tarjan($adjacentId);
127 $this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
128 } elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
129 $this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
130 }
131 }
132
133 $lowlink = $this->lowlink[$oid];
134 if ($lowlink === $this->dfs[$oid]) {
135 $representingNode = null;
136 do {
137 $unwindOid = array_pop($this->stack);
138
139 if (! $representingNode) {
140 $representingNode = $this->nodes[$unwindOid];
141 }
142
143 $this->representingNodes[$unwindOid] = $representingNode;
144 $this->states[$unwindOid] = self::VISITED;
145 } while ($unwindOid !== $oid);
146 }
147 }
148
149 public function getNodeRepresentingStronglyConnectedComponent(object $node): object
150 {
151 $oid = spl_object_id($node);
152
153 if (! isset($this->representingNodes[$oid])) {
154 throw new InvalidArgumentException('unknown node');
155 }
156
157 return $this->representingNodes[$oid];
158 }
159}
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort.php b/vendor/doctrine/orm/src/Internal/TopologicalSort.php
new file mode 100644
index 0000000..808bc0f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/TopologicalSort.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
8
9use function array_keys;
10use function spl_object_id;
11
12/**
13 * TopologicalSort implements topological sorting, which is an ordering
14 * algorithm for directed graphs (DG) using a depth-first searching (DFS)
15 * to traverse the graph built in memory.
16 * This algorithm has a linear running time based on nodes (V) and edges
17 * between the nodes (E), resulting in a computational complexity of O(V + E).
18 *
19 * @internal
20 */
21final class TopologicalSort
22{
23 private const NOT_VISITED = 1;
24 private const IN_PROGRESS = 2;
25 private const VISITED = 3;
26
27 /**
28 * Array of all nodes, indexed by object ids.
29 *
30 * @var array<int, object>
31 */
32 private array $nodes = [];
33
34 /**
35 * DFS state for the different nodes, indexed by node object id and using one of
36 * this class' constants as value.
37 *
38 * @var array<int, self::*>
39 */
40 private array $states = [];
41
42 /**
43 * Edges between the nodes. The first-level key is the object id of the outgoing
44 * node; the second array maps the destination node by object id as key. The final
45 * boolean value indicates whether the edge is optional or not.
46 *
47 * @var array<int, array<int, bool>>
48 */
49 private array $edges = [];
50
51 /**
52 * Builds up the result during the DFS.
53 *
54 * @var list<object>
55 */
56 private array $sortResult = [];
57
58 public function addNode(object $node): void
59 {
60 $id = spl_object_id($node);
61 $this->nodes[$id] = $node;
62 $this->states[$id] = self::NOT_VISITED;
63 $this->edges[$id] = [];
64 }
65
66 public function hasNode(object $node): bool
67 {
68 return isset($this->nodes[spl_object_id($node)]);
69 }
70
71 /**
72 * Adds a new edge between two nodes to the graph
73 *
74 * @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles.
75 */
76 public function addEdge(object $from, object $to, bool $optional): void
77 {
78 $fromId = spl_object_id($from);
79 $toId = spl_object_id($to);
80
81 if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) {
82 return; // we already know about this dependency, and it is not optional
83 }
84
85 $this->edges[$fromId][$toId] = $optional;
86 }
87
88 /**
89 * Returns a topological sort of all nodes. When we have an edge A->B between two nodes
90 * A and B, then B will be listed before A in the result. Visually speaking, when ordering
91 * the nodes in the result order from left to right, all edges point to the left.
92 *
93 * @return list<object>
94 */
95 public function sort(): array
96 {
97 foreach (array_keys($this->nodes) as $oid) {
98 if ($this->states[$oid] === self::NOT_VISITED) {
99 $this->visit($oid);
100 }
101 }
102
103 return $this->sortResult;
104 }
105
106 private function visit(int $oid): void
107 {
108 if ($this->states[$oid] === self::IN_PROGRESS) {
109 // This node is already on the current DFS stack. We've found a cycle!
110 throw new CycleDetectedException($this->nodes[$oid]);
111 }
112
113 if ($this->states[$oid] === self::VISITED) {
114 // We've reached a node that we've already seen, including all
115 // other nodes that are reachable from here. We're done here, return.
116 return;
117 }
118
119 $this->states[$oid] = self::IN_PROGRESS;
120
121 // Continue the DFS downwards the edge list
122 foreach ($this->edges[$oid] as $adjacentId => $optional) {
123 try {
124 $this->visit($adjacentId);
125 } catch (CycleDetectedException $exception) {
126 if ($exception->isCycleCollected()) {
127 // There is a complete cycle downstream of the current node. We cannot
128 // do anything about that anymore.
129 throw $exception;
130 }
131
132 if ($optional) {
133 // The current edge is part of a cycle, but it is optional and the closest
134 // such edge while backtracking. Break the cycle here by skipping the edge
135 // and continuing with the next one.
136 continue;
137 }
138
139 // We have found a cycle and cannot break it at $edge. Best we can do
140 // is to backtrack from the current vertex, hoping that somewhere up the
141 // stack this can be salvaged.
142 $this->states[$oid] = self::NOT_VISITED;
143 $exception->addToCycle($this->nodes[$oid]);
144
145 throw $exception;
146 }
147 }
148
149 // We have traversed all edges and visited all other nodes reachable from here.
150 // So we're done with this vertex as well.
151
152 $this->states[$oid] = self::VISITED;
153 $this->sortResult[] = $this->nodes[$oid];
154 }
155}
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php
new file mode 100644
index 0000000..3af5329
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\TopologicalSort;
6
7use RuntimeException;
8
9use function array_unshift;
10
11class CycleDetectedException extends RuntimeException
12{
13 /** @var list<object> */
14 private array $cycle;
15
16 /**
17 * Do we have the complete cycle collected?
18 */
19 private bool $cycleCollected = false;
20
21 public function __construct(private readonly object $startNode)
22 {
23 parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.');
24
25 $this->cycle = [$startNode];
26 }
27
28 /** @return list<object> */
29 public function getCycle(): array
30 {
31 return $this->cycle;
32 }
33
34 public function addToCycle(object $node): void
35 {
36 array_unshift($this->cycle, $node);
37
38 if ($node === $this->startNode) {
39 $this->cycleCollected = true;
40 }
41 }
42
43 public function isCycleCollected(): bool
44 {
45 return $this->cycleCollected;
46 }
47}
diff --git a/vendor/doctrine/orm/src/LazyCriteriaCollection.php b/vendor/doctrine/orm/src/LazyCriteriaCollection.php
new file mode 100644
index 0000000..ca67914
--- /dev/null
+++ b/vendor/doctrine/orm/src/LazyCriteriaCollection.php
@@ -0,0 +1,96 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\Common\Collections\AbstractLazyCollection;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\Common\Collections\Criteria;
10use Doctrine\Common\Collections\ReadableCollection;
11use Doctrine\Common\Collections\Selectable;
12use Doctrine\ORM\Persisters\Entity\EntityPersister;
13
14use function assert;
15
16/**
17 * A lazy collection that allows a fast count when using criteria object
18 * Once count gets executed once without collection being initialized, result
19 * is cached and returned on subsequent calls until collection gets loaded,
20 * then returning the number of loaded results.
21 *
22 * @template TKey of array-key
23 * @template TValue of object
24 * @extends AbstractLazyCollection<TKey, TValue>
25 * @implements Selectable<TKey, TValue>
26 */
27class LazyCriteriaCollection extends AbstractLazyCollection implements Selectable
28{
29 private int|null $count = null;
30
31 public function __construct(
32 protected EntityPersister $entityPersister,
33 protected Criteria $criteria,
34 ) {
35 }
36
37 /**
38 * Do an efficient count on the collection
39 */
40 public function count(): int
41 {
42 if ($this->isInitialized()) {
43 return $this->collection->count();
44 }
45
46 // Return cached result in case count query was already executed
47 if ($this->count !== null) {
48 return $this->count;
49 }
50
51 return $this->count = $this->entityPersister->count($this->criteria);
52 }
53
54 /**
55 * check if collection is empty without loading it
56 */
57 public function isEmpty(): bool
58 {
59 if ($this->isInitialized()) {
60 return $this->collection->isEmpty();
61 }
62
63 return ! $this->count();
64 }
65
66 /**
67 * Do an optimized search of an element
68 *
69 * @param mixed $element The element to search for.
70 *
71 * @return bool TRUE if the collection contains $element, FALSE otherwise.
72 */
73 public function contains(mixed $element): bool
74 {
75 if ($this->isInitialized()) {
76 return $this->collection->contains($element);
77 }
78
79 return $this->entityPersister->exists($element, $this->criteria);
80 }
81
82 /** @return ReadableCollection<TKey, TValue>&Selectable<TKey, TValue> */
83 public function matching(Criteria $criteria): ReadableCollection&Selectable
84 {
85 $this->initialize();
86 assert($this->collection instanceof Selectable);
87
88 return $this->collection->matching($criteria);
89 }
90
91 protected function doInitialize(): void
92 {
93 $elements = $this->entityPersister->loadCriteria($this->criteria);
94 $this->collection = new ArrayCollection($elements);
95 }
96}
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}
diff --git a/vendor/doctrine/orm/src/NativeQuery.php b/vendor/doctrine/orm/src/NativeQuery.php
new file mode 100644
index 0000000..6cee0e8
--- /dev/null
+++ b/vendor/doctrine/orm/src/NativeQuery.php
@@ -0,0 +1,68 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\DBAL\Result;
8use Doctrine\ORM\Query\ParameterTypeInferer;
9
10use function array_values;
11use function is_int;
12use function key;
13use function ksort;
14
15/**
16 * Represents a native SQL query.
17 *
18 * @final
19 */
20class NativeQuery extends AbstractQuery
21{
22 private string $sql;
23
24 /** @return $this */
25 public function setSQL(string $sql): self
26 {
27 $this->sql = $sql;
28
29 return $this;
30 }
31
32 public function getSQL(): string
33 {
34 return $this->sql;
35 }
36
37 protected function _doExecute(): Result|int
38 {
39 $parameters = [];
40 $types = [];
41
42 foreach ($this->getParameters() as $parameter) {
43 $name = $parameter->getName();
44 $value = $this->processParameterValue($parameter->getValue());
45 $type = $parameter->getValue() === $value
46 ? $parameter->getType()
47 : ParameterTypeInferer::inferType($value);
48
49 $parameters[$name] = $value;
50 $types[$name] = $type;
51 }
52
53 if ($parameters && is_int(key($parameters))) {
54 ksort($parameters);
55 ksort($types);
56
57 $parameters = array_values($parameters);
58 $types = array_values($types);
59 }
60
61 return $this->em->getConnection()->executeQuery(
62 $this->sql,
63 $parameters,
64 $types,
65 $this->queryCacheProfile,
66 );
67 }
68}
diff --git a/vendor/doctrine/orm/src/NoResultException.php b/vendor/doctrine/orm/src/NoResultException.php
new file mode 100644
index 0000000..6ecabae
--- /dev/null
+++ b/vendor/doctrine/orm/src/NoResultException.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7/**
8 * Exception thrown when an ORM query unexpectedly does not return any results.
9 */
10class NoResultException extends UnexpectedResultException
11{
12 public function __construct()
13 {
14 parent::__construct('No result was found for query although at least one row was expected.');
15 }
16}
diff --git a/vendor/doctrine/orm/src/NonUniqueResultException.php b/vendor/doctrine/orm/src/NonUniqueResultException.php
new file mode 100644
index 0000000..8f56e44
--- /dev/null
+++ b/vendor/doctrine/orm/src/NonUniqueResultException.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7/**
8 * Exception thrown when an ORM query unexpectedly returns more than one result.
9 */
10class NonUniqueResultException extends UnexpectedResultException
11{
12 public const DEFAULT_MESSAGE = 'More than one result was found for query although one row or none was expected.';
13
14 public function __construct(string|null $message = null)
15 {
16 parent::__construct($message ?? self::DEFAULT_MESSAGE);
17 }
18}
diff --git a/vendor/doctrine/orm/src/ORMInvalidArgumentException.php b/vendor/doctrine/orm/src/ORMInvalidArgumentException.php
new file mode 100644
index 0000000..fe07a2a
--- /dev/null
+++ b/vendor/doctrine/orm/src/ORMInvalidArgumentException.php
@@ -0,0 +1,195 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Mapping\AssociationMapping;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use InvalidArgumentException;
10use Stringable;
11
12use function array_map;
13use function count;
14use function get_debug_type;
15use function gettype;
16use function implode;
17use function is_scalar;
18use function reset;
19use function spl_object_id;
20use function sprintf;
21
22/**
23 * Contains exception messages for all invalid lifecycle state exceptions inside UnitOfWork
24 */
25class ORMInvalidArgumentException extends InvalidArgumentException
26{
27 public static function scheduleInsertForManagedEntity(object $entity): self
28 {
29 return new self('A managed+dirty entity ' . self::objToStr($entity) . ' can not be scheduled for insertion.');
30 }
31
32 public static function scheduleInsertForRemovedEntity(object $entity): self
33 {
34 return new self('Removed entity ' . self::objToStr($entity) . ' can not be scheduled for insertion.');
35 }
36
37 public static function scheduleInsertTwice(object $entity): self
38 {
39 return new self('Entity ' . self::objToStr($entity) . ' can not be scheduled for insertion twice.');
40 }
41
42 public static function entityWithoutIdentity(string $className, object $entity): self
43 {
44 return new self(
45 "The given entity of type '" . $className . "' (" . self::objToStr($entity) . ') has no identity/no ' .
46 'id values set. It cannot be added to the identity map.',
47 );
48 }
49
50 public static function readOnlyRequiresManagedEntity(object $entity): self
51 {
52 return new self('Only managed entities can be marked or checked as read only. But ' . self::objToStr($entity) . ' is not');
53 }
54
55 /** @param non-empty-list<array{AssociationMapping, object}> $newEntitiesWithAssociations */
56 public static function newEntitiesFoundThroughRelationships(array $newEntitiesWithAssociations): self
57 {
58 $errorMessages = array_map(
59 static function (array $newEntityWithAssociation): string {
60 [$associationMapping, $entity] = $newEntityWithAssociation;
61
62 return self::newEntityFoundThroughRelationshipMessage($associationMapping, $entity);
63 },
64 $newEntitiesWithAssociations,
65 );
66
67 if (count($errorMessages) === 1) {
68 return new self(reset($errorMessages));
69 }
70
71 return new self(
72 'Multiple non-persisted new entities were found through the given association graph:'
73 . "\n\n * "
74 . implode("\n * ", $errorMessages),
75 );
76 }
77
78 public static function newEntityFoundThroughRelationship(AssociationMapping $associationMapping, object $entry): self
79 {
80 return new self(self::newEntityFoundThroughRelationshipMessage($associationMapping, $entry));
81 }
82
83 public static function detachedEntityFoundThroughRelationship(AssociationMapping $assoc, object $entry): self
84 {
85 return new self('A detached entity of type ' . $assoc->targetEntity . ' (' . self::objToStr($entry) . ') '
86 . " was found through the relationship '" . $assoc->sourceEntity . '#' . $assoc->fieldName . "' "
87 . 'during cascading a persist operation.');
88 }
89
90 public static function entityNotManaged(object $entity): self
91 {
92 return new self('Entity ' . self::objToStr($entity) . ' is not managed. An entity is managed if its fetched ' .
93 'from the database or registered as new through EntityManager#persist');
94 }
95
96 public static function entityHasNoIdentity(object $entity, string $operation): self
97 {
98 return new self('Entity has no identity, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity));
99 }
100
101 public static function entityIsRemoved(object $entity, string $operation): self
102 {
103 return new self('Entity is removed, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity));
104 }
105
106 public static function detachedEntityCannot(object $entity, string $operation): self
107 {
108 return new self('Detached entity ' . self::objToStr($entity) . ' cannot be ' . $operation);
109 }
110
111 public static function invalidObject(string $context, mixed $given, int $parameterIndex = 1): self
112 {
113 return new self($context . ' expects parameter ' . $parameterIndex .
114 ' to be an entity object, ' . gettype($given) . ' given.');
115 }
116
117 public static function invalidCompositeIdentifier(): self
118 {
119 return new self('Binding an entity with a composite primary key to a query is not supported. ' .
120 'You should split the parameter into the explicit fields and bind them separately.');
121 }
122
123 public static function invalidIdentifierBindingEntity(string $class): self
124 {
125 return new self(sprintf(
126 <<<'EXCEPTION'
127Binding entities to query parameters only allowed for entities that have an identifier.
128Class "%s" does not have an identifier.
129EXCEPTION
130 ,
131 $class,
132 ));
133 }
134
135 public static function invalidAssociation(ClassMetadata $targetClass, AssociationMapping $assoc, mixed $actualValue): self
136 {
137 $expectedType = $targetClass->getName();
138
139 return new self(sprintf(
140 'Expected value of type "%s" for association field "%s#$%s", got "%s" instead.',
141 $expectedType,
142 $assoc->sourceEntity,
143 $assoc->fieldName,
144 get_debug_type($actualValue),
145 ));
146 }
147
148 public static function invalidAutoGenerateMode(mixed $value): self
149 {
150 return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value)));
151 }
152
153 public static function missingPrimaryKeyValue(string $className, string $idField): self
154 {
155 return new self(sprintf('Missing value for primary key %s on %s', $idField, $className));
156 }
157
158 public static function proxyDirectoryRequired(): self
159 {
160 return new self('You must configure a proxy directory. See docs for details');
161 }
162
163 public static function proxyNamespaceRequired(): self
164 {
165 return new self('You must configure a proxy namespace');
166 }
167
168 public static function proxyDirectoryNotWritable(string $proxyDirectory): self
169 {
170 return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory));
171 }
172
173 /**
174 * Helper method to show an object as string.
175 */
176 private static function objToStr(object $obj): string
177 {
178 return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj);
179 }
180
181 private static function newEntityFoundThroughRelationshipMessage(AssociationMapping $associationMapping, object $entity): string
182 {
183 return 'A new entity was found through the relationship \''
184 . $associationMapping->sourceEntity . '#' . $associationMapping->fieldName . '\' that was not'
185 . ' configured to cascade persist operations for entity: ' . self::objToStr($entity) . '.'
186 . ' To solve this issue: Either explicitly call EntityManager#persist()'
187 . ' on this unknown entity or configure cascade persist'
188 . ' this association in the mapping for example @ManyToOne(..,cascade={"persist"}).'
189 . ($entity instanceof Stringable
190 ? ''
191 : ' If you cannot find out which entity causes the problem implement \''
192 . $associationMapping->targetEntity . '#__toString()\' to get a clue.'
193 );
194 }
195}
diff --git a/vendor/doctrine/orm/src/ORMSetup.php b/vendor/doctrine/orm/src/ORMSetup.php
new file mode 100644
index 0000000..7354c71
--- /dev/null
+++ b/vendor/doctrine/orm/src/ORMSetup.php
@@ -0,0 +1,127 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Mapping\Driver\AttributeDriver;
8use Doctrine\ORM\Mapping\Driver\XmlDriver;
9use Psr\Cache\CacheItemPoolInterface;
10use Redis;
11use RuntimeException;
12use Symfony\Component\Cache\Adapter\ApcuAdapter;
13use Symfony\Component\Cache\Adapter\ArrayAdapter;
14use Symfony\Component\Cache\Adapter\MemcachedAdapter;
15use Symfony\Component\Cache\Adapter\RedisAdapter;
16
17use function apcu_enabled;
18use function class_exists;
19use function extension_loaded;
20use function md5;
21use function sys_get_temp_dir;
22
23final class ORMSetup
24{
25 /**
26 * Creates a configuration with an attribute metadata driver.
27 *
28 * @param string[] $paths
29 */
30 public static function createAttributeMetadataConfiguration(
31 array $paths,
32 bool $isDevMode = false,
33 string|null $proxyDir = null,
34 CacheItemPoolInterface|null $cache = null,
35 ): Configuration {
36 $config = self::createConfiguration($isDevMode, $proxyDir, $cache);
37 $config->setMetadataDriverImpl(new AttributeDriver($paths));
38
39 return $config;
40 }
41
42 /**
43 * Creates a configuration with an XML metadata driver.
44 *
45 * @param string[] $paths
46 */
47 public static function createXMLMetadataConfiguration(
48 array $paths,
49 bool $isDevMode = false,
50 string|null $proxyDir = null,
51 CacheItemPoolInterface|null $cache = null,
52 bool $isXsdValidationEnabled = true,
53 ): Configuration {
54 $config = self::createConfiguration($isDevMode, $proxyDir, $cache);
55 $config->setMetadataDriverImpl(new XmlDriver($paths, XmlDriver::DEFAULT_FILE_EXTENSION, $isXsdValidationEnabled));
56
57 return $config;
58 }
59
60 /**
61 * Creates a configuration without a metadata driver.
62 */
63 public static function createConfiguration(
64 bool $isDevMode = false,
65 string|null $proxyDir = null,
66 CacheItemPoolInterface|null $cache = null,
67 ): Configuration {
68 $proxyDir = $proxyDir ?: sys_get_temp_dir();
69
70 $cache = self::createCacheInstance($isDevMode, $proxyDir, $cache);
71
72 $config = new Configuration();
73
74 $config->setMetadataCache($cache);
75 $config->setQueryCache($cache);
76 $config->setResultCache($cache);
77 $config->setProxyDir($proxyDir);
78 $config->setProxyNamespace('DoctrineProxies');
79 $config->setAutoGenerateProxyClasses($isDevMode);
80
81 return $config;
82 }
83
84 private static function createCacheInstance(
85 bool $isDevMode,
86 string $proxyDir,
87 CacheItemPoolInterface|null $cache,
88 ): CacheItemPoolInterface {
89 if ($cache !== null) {
90 return $cache;
91 }
92
93 if (! class_exists(ArrayAdapter::class)) {
94 throw new RuntimeException(
95 'The Doctrine setup tool cannot configure caches without symfony/cache.'
96 . ' Please add symfony/cache as explicit dependency or pass your own cache implementation.',
97 );
98 }
99
100 if ($isDevMode) {
101 return new ArrayAdapter();
102 }
103
104 $namespace = 'dc2_' . md5($proxyDir);
105
106 if (extension_loaded('apcu') && apcu_enabled()) {
107 return new ApcuAdapter($namespace);
108 }
109
110 if (MemcachedAdapter::isSupported()) {
111 return new MemcachedAdapter(MemcachedAdapter::createConnection('memcached://127.0.0.1'), $namespace);
112 }
113
114 if (extension_loaded('redis')) {
115 $redis = new Redis();
116 $redis->connect('127.0.0.1');
117
118 return new RedisAdapter($redis, $namespace);
119 }
120
121 return new ArrayAdapter();
122 }
123
124 private function __construct()
125 {
126 }
127}
diff --git a/vendor/doctrine/orm/src/OptimisticLockException.php b/vendor/doctrine/orm/src/OptimisticLockException.php
new file mode 100644
index 0000000..f84e134
--- /dev/null
+++ b/vendor/doctrine/orm/src/OptimisticLockException.php
@@ -0,0 +1,55 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use DateTimeInterface;
8use Doctrine\ORM\Exception\ORMException;
9use Exception;
10use Throwable;
11
12/**
13 * An OptimisticLockException is thrown when a version check on an object
14 * that uses optimistic locking through a version field fails.
15 */
16class OptimisticLockException extends Exception implements ORMException
17{
18 public function __construct(
19 string $msg,
20 private readonly object|string|null $entity,
21 Throwable|null $previous = null,
22 ) {
23 parent::__construct($msg, 0, $previous);
24 }
25
26 /**
27 * Gets the entity that caused the exception.
28 */
29 public function getEntity(): object|string|null
30 {
31 return $this->entity;
32 }
33
34 /** @param object|class-string $entity */
35 public static function lockFailed(object|string $entity): self
36 {
37 return new self('The optimistic lock on an entity failed.', $entity);
38 }
39
40 public static function lockFailedVersionMismatch(
41 object $entity,
42 int|string|DateTimeInterface $expectedLockVersion,
43 int|string|DateTimeInterface $actualLockVersion,
44 ): self {
45 $expectedLockVersion = $expectedLockVersion instanceof DateTimeInterface ? $expectedLockVersion->getTimestamp() : $expectedLockVersion;
46 $actualLockVersion = $actualLockVersion instanceof DateTimeInterface ? $actualLockVersion->getTimestamp() : $actualLockVersion;
47
48 return new self('The optimistic lock failed, version ' . $expectedLockVersion . ' was expected, but is actually ' . $actualLockVersion, $entity);
49 }
50
51 public static function notVersioned(string $entityName): self
52 {
53 return new self('Cannot obtain optimistic lock on unversioned entity ' . $entityName, null);
54 }
55}
diff --git a/vendor/doctrine/orm/src/PersistentCollection.php b/vendor/doctrine/orm/src/PersistentCollection.php
new file mode 100644
index 0000000..d54d3d1
--- /dev/null
+++ b/vendor/doctrine/orm/src/PersistentCollection.php
@@ -0,0 +1,652 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\Common\Collections\AbstractLazyCollection;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\Common\Collections\Criteria;
11use Doctrine\Common\Collections\Order;
12use Doctrine\Common\Collections\Selectable;
13use Doctrine\ORM\Mapping\AssociationMapping;
14use Doctrine\ORM\Mapping\ClassMetadata;
15use Doctrine\ORM\Mapping\ToManyAssociationMapping;
16use RuntimeException;
17use UnexpectedValueException;
18
19use function array_combine;
20use function array_diff_key;
21use function array_map;
22use function array_values;
23use function array_walk;
24use function assert;
25use function is_object;
26use function spl_object_id;
27use function strtoupper;
28
29/**
30 * A PersistentCollection represents a collection of elements that have persistent state.
31 *
32 * Collections of entities represent only the associations (links) to those entities.
33 * That means, if the collection is part of a many-many mapping and you remove
34 * entities from the collection, only the links in the relation table are removed (on flush).
35 * Similarly, if you remove entities from a collection that is part of a one-many
36 * mapping this will only result in the nulling out of the foreign keys on flush.
37 *
38 * @psalm-template TKey of array-key
39 * @psalm-template T
40 * @template-extends AbstractLazyCollection<TKey,T>
41 * @template-implements Selectable<TKey,T>
42 */
43final class PersistentCollection extends AbstractLazyCollection implements Selectable
44{
45 /**
46 * A snapshot of the collection at the moment it was fetched from the database.
47 * This is used to create a diff of the collection at commit time.
48 *
49 * @psalm-var array<string|int, mixed>
50 */
51 private array $snapshot = [];
52
53 /**
54 * The entity that owns this collection.
55 */
56 private object|null $owner = null;
57
58 /**
59 * The association mapping the collection belongs to.
60 * This is currently either a OneToManyMapping or a ManyToManyMapping.
61 *
62 * @var (AssociationMapping&ToManyAssociationMapping)|null
63 */
64 private AssociationMapping|null $association = null;
65
66 /**
67 * The name of the field on the target entities that points to the owner
68 * of the collection. This is only set if the association is bi-directional.
69 */
70 private string|null $backRefFieldName = null;
71
72 /**
73 * Whether the collection is dirty and needs to be synchronized with the database
74 * when the UnitOfWork that manages its persistent state commits.
75 */
76 private bool $isDirty = false;
77
78 /**
79 * Creates a new persistent collection.
80 *
81 * @param EntityManagerInterface $em The EntityManager the collection will be associated with.
82 * @param ClassMetadata $typeClass The class descriptor of the entity type of this collection.
83 * @psalm-param Collection<TKey, T>&Selectable<TKey, T> $collection The collection elements.
84 */
85 public function __construct(
86 private EntityManagerInterface|null $em,
87 private readonly ClassMetadata|null $typeClass,
88 Collection $collection,
89 ) {
90 $this->collection = $collection;
91 $this->initialized = true;
92 }
93
94 /**
95 * INTERNAL:
96 * Sets the collection's owning entity together with the AssociationMapping that
97 * describes the association between the owner and the elements of the collection.
98 */
99 public function setOwner(object $entity, AssociationMapping&ToManyAssociationMapping $assoc): void
100 {
101 $this->owner = $entity;
102 $this->association = $assoc;
103 $this->backRefFieldName = $assoc->isOwningSide() ? $assoc->inversedBy : $assoc->mappedBy;
104 }
105
106 /**
107 * INTERNAL:
108 * Gets the collection owner.
109 */
110 public function getOwner(): object|null
111 {
112 return $this->owner;
113 }
114
115 public function getTypeClass(): ClassMetadata
116 {
117 assert($this->typeClass !== null);
118
119 return $this->typeClass;
120 }
121
122 private function getUnitOfWork(): UnitOfWork
123 {
124 assert($this->em !== null);
125
126 return $this->em->getUnitOfWork();
127 }
128
129 /**
130 * INTERNAL:
131 * Adds an element to a collection during hydration. This will automatically
132 * complete bidirectional associations in the case of a one-to-many association.
133 */
134 public function hydrateAdd(mixed $element): void
135 {
136 $this->unwrap()->add($element);
137
138 // If _backRefFieldName is set and its a one-to-many association,
139 // we need to set the back reference.
140 if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
141 assert($this->typeClass !== null);
142 // Set back reference to owner
143 $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
144 $element,
145 $this->owner,
146 );
147
148 $this->getUnitOfWork()->setOriginalEntityProperty(
149 spl_object_id($element),
150 $this->backRefFieldName,
151 $this->owner,
152 );
153 }
154 }
155
156 /**
157 * INTERNAL:
158 * Sets a keyed element in the collection during hydration.
159 */
160 public function hydrateSet(mixed $key, mixed $element): void
161 {
162 $this->unwrap()->set($key, $element);
163
164 // If _backRefFieldName is set, then the association is bidirectional
165 // and we need to set the back reference.
166 if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) {
167 assert($this->typeClass !== null);
168 // Set back reference to owner
169 $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
170 $element,
171 $this->owner,
172 );
173 }
174 }
175
176 /**
177 * Initializes the collection by loading its contents from the database
178 * if the collection is not yet initialized.
179 */
180 public function initialize(): void
181 {
182 if ($this->initialized || ! $this->association) {
183 return;
184 }
185
186 $this->doInitialize();
187
188 $this->initialized = true;
189 }
190
191 /**
192 * INTERNAL:
193 * Tells this collection to take a snapshot of its current state.
194 */
195 public function takeSnapshot(): void
196 {
197 $this->snapshot = $this->unwrap()->toArray();
198 $this->isDirty = false;
199 }
200
201 /**
202 * INTERNAL:
203 * Returns the last snapshot of the elements in the collection.
204 *
205 * @psalm-return array<string|int, mixed> The last snapshot of the elements.
206 */
207 public function getSnapshot(): array
208 {
209 return $this->snapshot;
210 }
211
212 /**
213 * INTERNAL:
214 * getDeleteDiff
215 *
216 * @return mixed[]
217 */
218 public function getDeleteDiff(): array
219 {
220 $collectionItems = $this->unwrap()->toArray();
221
222 return array_values(array_diff_key(
223 array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot),
224 array_combine(array_map('spl_object_id', $collectionItems), $collectionItems),
225 ));
226 }
227
228 /**
229 * INTERNAL:
230 * getInsertDiff
231 *
232 * @return mixed[]
233 */
234 public function getInsertDiff(): array
235 {
236 $collectionItems = $this->unwrap()->toArray();
237
238 return array_values(array_diff_key(
239 array_combine(array_map('spl_object_id', $collectionItems), $collectionItems),
240 array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot),
241 ));
242 }
243
244 /** INTERNAL: Gets the association mapping of the collection. */
245 public function getMapping(): AssociationMapping&ToManyAssociationMapping
246 {
247 if ($this->association === null) {
248 throw new UnexpectedValueException('The underlying association mapping is null although it should not be');
249 }
250
251 return $this->association;
252 }
253
254 /**
255 * Marks this collection as changed/dirty.
256 */
257 private function changed(): void
258 {
259 if ($this->isDirty) {
260 return;
261 }
262
263 $this->isDirty = true;
264 }
265
266 /**
267 * Gets a boolean flag indicating whether this collection is dirty which means
268 * its state needs to be synchronized with the database.
269 */
270 public function isDirty(): bool
271 {
272 return $this->isDirty;
273 }
274
275 /**
276 * Sets a boolean flag, indicating whether this collection is dirty.
277 */
278 public function setDirty(bool $dirty): void
279 {
280 $this->isDirty = $dirty;
281 }
282
283 /**
284 * Sets the initialized flag of the collection, forcing it into that state.
285 */
286 public function setInitialized(bool $bool): void
287 {
288 $this->initialized = $bool;
289 }
290
291 public function remove(string|int $key): mixed
292 {
293 // TODO: If the keys are persistent as well (not yet implemented)
294 // and the collection is not initialized and orphanRemoval is
295 // not used we can issue a straight SQL delete/update on the
296 // association (table). Without initializing the collection.
297 $removed = parent::remove($key);
298
299 if (! $removed) {
300 return $removed;
301 }
302
303 $this->changed();
304
305 if (
306 $this->association !== null &&
307 $this->association->isToMany() &&
308 $this->owner &&
309 $this->getMapping()->orphanRemoval
310 ) {
311 $this->getUnitOfWork()->scheduleOrphanRemoval($removed);
312 }
313
314 return $removed;
315 }
316
317 public function removeElement(mixed $element): bool
318 {
319 $removed = parent::removeElement($element);
320
321 if (! $removed) {
322 return $removed;
323 }
324
325 $this->changed();
326
327 if (
328 $this->association !== null &&
329 $this->association->isToMany() &&
330 $this->owner &&
331 $this->getMapping()->orphanRemoval
332 ) {
333 $this->getUnitOfWork()->scheduleOrphanRemoval($element);
334 }
335
336 return $removed;
337 }
338
339 public function containsKey(mixed $key): bool
340 {
341 if (
342 ! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY
343 && isset($this->getMapping()->indexBy)
344 ) {
345 $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
346
347 return $this->unwrap()->containsKey($key) || $persister->containsKey($this, $key);
348 }
349
350 return parent::containsKey($key);
351 }
352
353 public function contains(mixed $element): bool
354 {
355 if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
356 $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
357
358 return $this->unwrap()->contains($element) || $persister->contains($this, $element);
359 }
360
361 return parent::contains($element);
362 }
363
364 public function get(string|int $key): mixed
365 {
366 if (
367 ! $this->initialized
368 && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY
369 && isset($this->getMapping()->indexBy)
370 ) {
371 assert($this->em !== null);
372 assert($this->typeClass !== null);
373 if (! $this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()->indexBy)) {
374 return $this->em->find($this->typeClass->name, $key);
375 }
376
377 return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this, $key);
378 }
379
380 return parent::get($key);
381 }
382
383 public function count(): int
384 {
385 if (! $this->initialized && $this->association !== null && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
386 $persister = $this->getUnitOfWork()->getCollectionPersister($this->association);
387
388 return $persister->count($this) + ($this->isDirty ? $this->unwrap()->count() : 0);
389 }
390
391 return parent::count();
392 }
393
394 public function set(string|int $key, mixed $value): void
395 {
396 parent::set($key, $value);
397
398 $this->changed();
399
400 if (is_object($value) && $this->em) {
401 $this->getUnitOfWork()->cancelOrphanRemoval($value);
402 }
403 }
404
405 public function add(mixed $value): bool
406 {
407 $this->unwrap()->add($value);
408
409 $this->changed();
410
411 if (is_object($value) && $this->em) {
412 $this->getUnitOfWork()->cancelOrphanRemoval($value);
413 }
414
415 return true;
416 }
417
418 public function offsetExists(mixed $offset): bool
419 {
420 return $this->containsKey($offset);
421 }
422
423 public function offsetGet(mixed $offset): mixed
424 {
425 return $this->get($offset);
426 }
427
428 public function offsetSet(mixed $offset, mixed $value): void
429 {
430 if (! isset($offset)) {
431 $this->add($value);
432
433 return;
434 }
435
436 $this->set($offset, $value);
437 }
438
439 public function offsetUnset(mixed $offset): void
440 {
441 $this->remove($offset);
442 }
443
444 public function isEmpty(): bool
445 {
446 return $this->unwrap()->isEmpty() && $this->count() === 0;
447 }
448
449 public function clear(): void
450 {
451 if ($this->initialized && $this->isEmpty()) {
452 $this->unwrap()->clear();
453
454 return;
455 }
456
457 $uow = $this->getUnitOfWork();
458 $association = $this->getMapping();
459
460 if (
461 $association->isToMany() &&
462 $association->orphanRemoval &&
463 $this->owner
464 ) {
465 // we need to initialize here, as orphan removal acts like implicit cascadeRemove,
466 // hence for event listeners we need the objects in memory.
467 $this->initialize();
468
469 foreach ($this->unwrap() as $element) {
470 $uow->scheduleOrphanRemoval($element);
471 }
472 }
473
474 $this->unwrap()->clear();
475
476 $this->initialized = true; // direct call, {@link initialize()} is too expensive
477
478 if ($association->isOwningSide() && $this->owner) {
479 $this->changed();
480
481 $uow->scheduleCollectionDeletion($this);
482
483 $this->takeSnapshot();
484 }
485 }
486
487 /**
488 * Called by PHP when this collection is serialized. Ensures that only the
489 * elements are properly serialized.
490 *
491 * Internal note: Tried to implement Serializable first but that did not work well
492 * with circular references. This solution seems simpler and works well.
493 *
494 * @return string[]
495 * @psalm-return array{0: string, 1: string}
496 */
497 public function __sleep(): array
498 {
499 return ['collection', 'initialized'];
500 }
501
502 public function __wakeup(): void
503 {
504 $this->em = null;
505 }
506
507 /**
508 * Extracts a slice of $length elements starting at position $offset from the Collection.
509 *
510 * If $length is null it returns all elements from $offset to the end of the Collection.
511 * Keys have to be preserved by this method. Calling this method will only return the
512 * selected slice and NOT change the elements contained in the collection slice is called on.
513 *
514 * @return mixed[]
515 * @psalm-return array<TKey,T>
516 */
517 public function slice(int $offset, int|null $length = null): array
518 {
519 if (! $this->initialized && ! $this->isDirty && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
520 $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
521
522 return $persister->slice($this, $offset, $length);
523 }
524
525 return parent::slice($offset, $length);
526 }
527
528 /**
529 * Cleans up internal state of cloned persistent collection.
530 *
531 * The following problems have to be prevented:
532 * 1. Added entities are added to old PC
533 * 2. New collection is not dirty, if reused on other entity nothing
534 * changes.
535 * 3. Snapshot leads to invalid diffs being generated.
536 * 4. Lazy loading grabs entities from old owner object.
537 * 5. New collection is connected to old owner and leads to duplicate keys.
538 */
539 public function __clone()
540 {
541 if (is_object($this->collection)) {
542 $this->collection = clone $this->collection;
543 }
544
545 $this->initialize();
546
547 $this->owner = null;
548 $this->snapshot = [];
549
550 $this->changed();
551 }
552
553 /**
554 * Selects all elements from a selectable that match the expression and
555 * return a new collection containing these elements.
556 *
557 * @psalm-return Collection<TKey, T>
558 *
559 * @throws RuntimeException
560 */
561 public function matching(Criteria $criteria): Collection
562 {
563 if ($this->isDirty) {
564 $this->initialize();
565 }
566
567 if ($this->initialized) {
568 return $this->unwrap()->matching($criteria);
569 }
570
571 $association = $this->getMapping();
572 if ($association->isManyToMany()) {
573 $persister = $this->getUnitOfWork()->getCollectionPersister($association);
574
575 return new ArrayCollection($persister->loadCriteria($this, $criteria));
576 }
577
578 $builder = Criteria::expr();
579 $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner);
580 $expression = $criteria->getWhereExpression();
581 $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression;
582
583 $criteria = clone $criteria;
584 $criteria->where($expression);
585 $criteria->orderBy(
586 $criteria->orderings() ?: array_map(
587 static fn (string $order): Order => Order::from(strtoupper($order)),
588 $association->orderBy(),
589 ),
590 );
591
592 $persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity);
593
594 return $association->fetch === ClassMetadata::FETCH_EXTRA_LAZY
595 ? new LazyCriteriaCollection($persister, $criteria)
596 : new ArrayCollection($persister->loadCriteria($criteria));
597 }
598
599 /**
600 * Retrieves the wrapped Collection instance.
601 *
602 * @return Collection<TKey, T>&Selectable<TKey, T>
603 */
604 public function unwrap(): Selectable&Collection
605 {
606 assert($this->collection instanceof Collection);
607 assert($this->collection instanceof Selectable);
608
609 return $this->collection;
610 }
611
612 protected function doInitialize(): void
613 {
614 // Has NEW objects added through add(). Remember them.
615 $newlyAddedDirtyObjects = [];
616
617 if ($this->isDirty) {
618 $newlyAddedDirtyObjects = $this->unwrap()->toArray();
619 }
620
621 $this->unwrap()->clear();
622 $this->getUnitOfWork()->loadCollection($this);
623 $this->takeSnapshot();
624
625 if ($newlyAddedDirtyObjects) {
626 $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects);
627 }
628 }
629
630 /**
631 * @param object[] $newObjects
632 *
633 * Note: the only reason why this entire looping/complexity is performed via `spl_object_id`
634 * is because we want to prevent using `array_udiff()`, which is likely to cause very
635 * high overhead (complexity of O(n^2)). `array_diff_key()` performs the operation in
636 * core, which is faster than using a callback for comparisons
637 */
638 private function restoreNewObjectsInDirtyCollection(array $newObjects): void
639 {
640 $loadedObjects = $this->unwrap()->toArray();
641 $newObjectsByOid = array_combine(array_map('spl_object_id', $newObjects), $newObjects);
642 $loadedObjectsByOid = array_combine(array_map('spl_object_id', $loadedObjects), $loadedObjects);
643 $newObjectsThatWereNotLoaded = array_diff_key($newObjectsByOid, $loadedObjectsByOid);
644
645 if ($newObjectsThatWereNotLoaded) {
646 // Reattach NEW objects added through add(), if any.
647 array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']);
648
649 $this->isDirty = true;
650 }
651 }
652}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php
new file mode 100644
index 0000000..26f0b9e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\QuoteStrategy;
11use Doctrine\ORM\UnitOfWork;
12
13/**
14 * Base class for all collection persisters.
15 */
16abstract class AbstractCollectionPersister implements CollectionPersister
17{
18 protected Connection $conn;
19 protected UnitOfWork $uow;
20 protected AbstractPlatform $platform;
21 protected QuoteStrategy $quoteStrategy;
22
23 /**
24 * Initializes a new instance of a class derived from AbstractCollectionPersister.
25 */
26 public function __construct(
27 protected EntityManagerInterface $em,
28 ) {
29 $this->uow = $em->getUnitOfWork();
30 $this->conn = $em->getConnection();
31 $this->platform = $this->conn->getDatabasePlatform();
32 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
33 }
34
35 /**
36 * Check if entity is in a valid state for operations.
37 */
38 protected function isValidEntityState(object $entity): bool
39 {
40 $entityState = $this->uow->getEntityState($entity, UnitOfWork::STATE_NEW);
41
42 if ($entityState === UnitOfWork::STATE_NEW) {
43 return false;
44 }
45
46 // If Entity is scheduled for inclusion, it is not in this collection.
47 // We can assure that because it would have return true before on array check
48 return ! ($entityState === UnitOfWork::STATE_MANAGED && $this->uow->isScheduledForInsert($entity));
49 }
50}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php
new file mode 100644
index 0000000..07c4eaf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php
@@ -0,0 +1,59 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\ORM\PersistentCollection;
9
10/**
11 * Define the behavior that should be implemented by all collection persisters.
12 */
13interface CollectionPersister
14{
15 /**
16 * Deletes the persistent state represented by the given collection.
17 */
18 public function delete(PersistentCollection $collection): void;
19
20 /**
21 * Updates the given collection, synchronizing its state with the database
22 * by inserting, updating and deleting individual elements.
23 */
24 public function update(PersistentCollection $collection): void;
25
26 /**
27 * Counts the size of this persistent collection.
28 */
29 public function count(PersistentCollection $collection): int;
30
31 /**
32 * Slices elements.
33 *
34 * @return mixed[]
35 */
36 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array;
37
38 /**
39 * Checks for existence of an element.
40 */
41 public function contains(PersistentCollection $collection, object $element): bool;
42
43 /**
44 * Checks for existence of a key.
45 */
46 public function containsKey(PersistentCollection $collection, mixed $key): bool;
47
48 /**
49 * Gets an element by key.
50 */
51 public function get(PersistentCollection $collection, mixed $index): mixed;
52
53 /**
54 * Loads association entities matching the given Criteria object.
55 *
56 * @return mixed[]
57 */
58 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array;
59}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php
new file mode 100644
index 0000000..7cf993d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php
@@ -0,0 +1,770 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use BadMethodCallException;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\Common\Collections\Expr\Comparison;
10use Doctrine\DBAL\Exception as DBALException;
11use Doctrine\DBAL\LockMode;
12use Doctrine\ORM\Mapping\AssociationMapping;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Mapping\InverseSideMapping;
15use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
16use Doctrine\ORM\PersistentCollection;
17use Doctrine\ORM\Persisters\SqlValueVisitor;
18use Doctrine\ORM\Query;
19use Doctrine\ORM\Utility\PersisterHelper;
20
21use function array_fill;
22use function array_pop;
23use function assert;
24use function count;
25use function implode;
26use function in_array;
27use function reset;
28use function sprintf;
29
30/**
31 * Persister for many-to-many collections.
32 */
33class ManyToManyPersister extends AbstractCollectionPersister
34{
35 public function delete(PersistentCollection $collection): void
36 {
37 $mapping = $this->getMapping($collection);
38
39 if (! $mapping->isOwningSide()) {
40 return; // ignore inverse side
41 }
42
43 assert($mapping->isManyToManyOwningSide());
44
45 $types = [];
46 $class = $this->em->getClassMetadata($mapping->sourceEntity);
47
48 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
49 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
50 }
51
52 $this->conn->executeStatement($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types);
53 }
54
55 public function update(PersistentCollection $collection): void
56 {
57 $mapping = $this->getMapping($collection);
58
59 if (! $mapping->isOwningSide()) {
60 return; // ignore inverse side
61 }
62
63 [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection);
64 [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection);
65
66 foreach ($collection->getDeleteDiff() as $element) {
67 $this->conn->executeStatement(
68 $deleteSql,
69 $this->getDeleteRowSQLParameters($collection, $element),
70 $deleteTypes,
71 );
72 }
73
74 foreach ($collection->getInsertDiff() as $element) {
75 $this->conn->executeStatement(
76 $insertSql,
77 $this->getInsertRowSQLParameters($collection, $element),
78 $insertTypes,
79 );
80 }
81 }
82
83 public function get(PersistentCollection $collection, mixed $index): object|null
84 {
85 $mapping = $this->getMapping($collection);
86
87 if (! $mapping->isIndexed()) {
88 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
89 }
90
91 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
92 $mappedKey = $mapping->isOwningSide()
93 ? $mapping->inversedBy
94 : $mapping->mappedBy;
95
96 assert($mappedKey !== null);
97
98 return $persister->load(
99 [$mappedKey => $collection->getOwner(), $mapping->indexBy() => $index],
100 null,
101 $mapping,
102 [],
103 LockMode::NONE,
104 1,
105 );
106 }
107
108 public function count(PersistentCollection $collection): int
109 {
110 $conditions = [];
111 $params = [];
112 $types = [];
113 $mapping = $this->getMapping($collection);
114 $id = $this->uow->getEntityIdentifier($collection->getOwner());
115 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
116 $association = $this->em->getMetadataFactory()->getOwningSide($mapping);
117
118 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform);
119 $joinColumns = ! $mapping->isOwningSide()
120 ? $association->joinTable->inverseJoinColumns
121 : $association->joinTable->joinColumns;
122
123 foreach ($joinColumns as $joinColumn) {
124 $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform);
125 $referencedName = $joinColumn->referencedColumnName;
126 $conditions[] = 't.' . $columnName . ' = ?';
127 $params[] = $id[$sourceClass->getFieldForColumn($referencedName)];
128 $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em);
129 }
130
131 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($mapping);
132
133 if ($filterSql) {
134 $conditions[] = $filterSql;
135 }
136
137 // If there is a provided criteria, make part of conditions
138 // @todo Fix this. Current SQL returns something like:
139 /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
140 // A join is needed on the target entity
141 $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
142 $targetJoinSql = ' JOIN ' . $targetTableName . ' te'
143 . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
144
145 // And criteria conditions needs to be added
146 $persister = $this->uow->getEntityPersister($targetClass->name);
147 $visitor = new SqlExpressionVisitor($persister, $targetClass);
148 $conditions[] = $visitor->dispatch($expression);
149
150 $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
151 }*/
152
153 $sql = 'SELECT COUNT(*)'
154 . ' FROM ' . $joinTableName . ' t'
155 . $joinTargetEntitySQL
156 . ' WHERE ' . implode(' AND ', $conditions);
157
158 return (int) $this->conn->fetchOne($sql, $params, $types);
159 }
160
161 /**
162 * {@inheritDoc}
163 */
164 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
165 {
166 $mapping = $this->getMapping($collection);
167 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
168
169 return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length);
170 }
171
172 public function containsKey(PersistentCollection $collection, mixed $key): bool
173 {
174 $mapping = $this->getMapping($collection);
175
176 if (! $mapping->isIndexed()) {
177 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
178 }
179
180 [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey(
181 $collection,
182 (string) $key,
183 true,
184 );
185
186 $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
187
188 return (bool) $this->conn->fetchOne($sql, $params, $types);
189 }
190
191 public function contains(PersistentCollection $collection, object $element): bool
192 {
193 if (! $this->isValidEntityState($element)) {
194 return false;
195 }
196
197 [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions(
198 $collection,
199 $element,
200 true,
201 );
202
203 $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
204
205 return (bool) $this->conn->fetchOne($sql, $params, $types);
206 }
207
208 /**
209 * {@inheritDoc}
210 */
211 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
212 {
213 $mapping = $this->getMapping($collection);
214 $owner = $collection->getOwner();
215 $ownerMetadata = $this->em->getClassMetadata($owner::class);
216 $id = $this->uow->getEntityIdentifier($owner);
217 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
218 $onConditions = $this->getOnConditionSQL($mapping);
219 $whereClauses = $params = [];
220 $paramTypes = [];
221
222 if (! $mapping->isOwningSide()) {
223 assert($mapping instanceof InverseSideMapping);
224 $associationSourceClass = $targetClass;
225 $sourceRelationMode = 'relationToTargetKeyColumns';
226 } else {
227 $associationSourceClass = $ownerMetadata;
228 $sourceRelationMode = 'relationToSourceKeyColumns';
229 }
230
231 $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping);
232
233 foreach ($mapping->$sourceRelationMode as $key => $value) {
234 $whereClauses[] = sprintf('t.%s = ?', $key);
235 $params[] = $ownerMetadata->containsForeignIdentifier
236 ? $id[$ownerMetadata->getFieldForColumn($value)]
237 : $id[$ownerMetadata->fieldNames[$value]];
238 $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em);
239 }
240
241 $parameters = $this->expandCriteriaParameters($criteria);
242
243 foreach ($parameters as $parameter) {
244 [$name, $value, $operator] = $parameter;
245
246 $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
247
248 if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
249 $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT');
250 } else {
251 $whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
252 $params[] = $value;
253 $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
254 }
255 }
256
257 $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
258 $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform);
259
260 $rsm = new Query\ResultSetMappingBuilder($this->em);
261 $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te');
262
263 $sql = 'SELECT ' . $rsm->generateSelectClause()
264 . ' FROM ' . $tableName . ' te'
265 . ' JOIN ' . $joinTable . ' t ON'
266 . implode(' AND ', $onConditions)
267 . ' WHERE ' . implode(' AND ', $whereClauses);
268
269 $sql .= $this->getOrderingSql($criteria, $targetClass);
270
271 $sql .= $this->getLimitSql($criteria);
272
273 $stmt = $this->conn->executeQuery($sql, $params, $paramTypes);
274
275 return $this
276 ->em
277 ->newHydrator(Query::HYDRATE_OBJECT)
278 ->hydrateAll($stmt, $rsm);
279 }
280
281 /**
282 * Generates the filter SQL for a given mapping.
283 *
284 * This method is not used for actually grabbing the related entities
285 * but when the extra-lazy collection methods are called on a filtered
286 * association. This is why besides the many to many table we also
287 * have to join in the actual entities table leading to additional
288 * JOIN.
289 *
290 * @param AssociationMapping $mapping Array containing mapping information.
291 *
292 * @return string[] ordered tuple:
293 * - JOIN condition to add to the SQL
294 * - WHERE condition to add to the SQL
295 * @psalm-return array{0: string, 1: string}
296 */
297 public function getFilterSql(AssociationMapping $mapping): array
298 {
299 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
300 $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName);
301 $filterSql = $this->generateFilterConditionSQL($rootClass, 'te');
302
303 if ($filterSql === '') {
304 return ['', ''];
305 }
306
307 // A join is needed if there is filtering on the target entity
308 $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
309 $joinSql = ' JOIN ' . $tableName . ' te'
310 . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping));
311
312 return [$joinSql, $filterSql];
313 }
314
315 /**
316 * Generates the filter SQL for a given entity and table alias.
317 *
318 * @param ClassMetadata $targetEntity Metadata of the target entity.
319 * @param string $targetTableAlias The table alias of the joined/selected table.
320 *
321 * @return string The SQL query part to add to a query.
322 */
323 protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string
324 {
325 $filterClauses = [];
326
327 foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
328 $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
329 if ($filterExpr) {
330 $filterClauses[] = '(' . $filterExpr . ')';
331 }
332 }
333
334 return $filterClauses
335 ? '(' . implode(' AND ', $filterClauses) . ')'
336 : '';
337 }
338
339 /**
340 * Generate ON condition
341 *
342 * @return string[]
343 * @psalm-return list<string>
344 */
345 protected function getOnConditionSQL(AssociationMapping $mapping): array
346 {
347 $association = $this->em->getMetadataFactory()->getOwningSide($mapping);
348 $joinColumns = $mapping->isOwningSide()
349 ? $association->joinTable->inverseJoinColumns
350 : $association->joinTable->joinColumns;
351
352 $conditions = [];
353
354 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
355 foreach ($joinColumns as $joinColumn) {
356 $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
357 $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
358
359 $conditions[] = ' t.' . $joinColumnName . ' = te.' . $refColumnName;
360 }
361
362 return $conditions;
363 }
364
365 protected function getDeleteSQL(PersistentCollection $collection): string
366 {
367 $columns = [];
368 $mapping = $this->getMapping($collection);
369 assert($mapping->isManyToManyOwningSide());
370 $class = $this->em->getClassMetadata($collection->getOwner()::class);
371 $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
372
373 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
374 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
375 }
376
377 return 'DELETE FROM ' . $joinTable
378 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
379 }
380
381 /**
382 * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql.
383 *
384 * @return list<mixed>
385 */
386 protected function getDeleteSQLParameters(PersistentCollection $collection): array
387 {
388 $mapping = $this->getMapping($collection);
389 assert($mapping->isManyToManyOwningSide());
390 $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
391
392 // Optimization for single column identifier
393 if (count($mapping->relationToSourceKeyColumns) === 1) {
394 return [reset($identifier)];
395 }
396
397 // Composite identifier
398 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
399 $params = [];
400
401 foreach ($mapping->relationToSourceKeyColumns as $columnName => $refColumnName) {
402 $params[] = isset($sourceClass->fieldNames[$refColumnName])
403 ? $identifier[$sourceClass->fieldNames[$refColumnName]]
404 : $identifier[$sourceClass->getFieldForColumn($refColumnName)];
405 }
406
407 return $params;
408 }
409
410 /**
411 * Gets the SQL statement used for deleting a row from the collection.
412 *
413 * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
414 * of types for bound parameters
415 * @psalm-return array{0: string, 1: list<string>}
416 */
417 protected function getDeleteRowSQL(PersistentCollection $collection): array
418 {
419 $mapping = $this->getMapping($collection);
420 assert($mapping->isManyToManyOwningSide());
421 $class = $this->em->getClassMetadata($mapping->sourceEntity);
422 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
423 $columns = [];
424 $types = [];
425
426 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
427 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
428 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
429 }
430
431 foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) {
432 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
433 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
434 }
435
436 return [
437 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
438 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?',
439 $types,
440 ];
441 }
442
443 /**
444 * Gets the SQL parameters for the corresponding SQL statement to delete the given
445 * element from the given collection.
446 *
447 * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql.
448 *
449 * @return mixed[]
450 * @psalm-return list<mixed>
451 */
452 protected function getDeleteRowSQLParameters(PersistentCollection $collection, object $element): array
453 {
454 return $this->collectJoinTableColumnParameters($collection, $element);
455 }
456
457 /**
458 * Gets the SQL statement used for inserting a row in the collection.
459 *
460 * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
461 * of types for bound parameters
462 * @psalm-return array{0: string, 1: list<string>}
463 */
464 protected function getInsertRowSQL(PersistentCollection $collection): array
465 {
466 $columns = [];
467 $types = [];
468 $mapping = $this->getMapping($collection);
469 assert($mapping->isManyToManyOwningSide());
470 $class = $this->em->getClassMetadata($mapping->sourceEntity);
471 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
472
473 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
474 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
475 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
476 }
477
478 foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) {
479 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
480 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
481 }
482
483 return [
484 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
485 . ' (' . implode(', ', $columns) . ')'
486 . ' VALUES'
487 . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')',
488 $types,
489 ];
490 }
491
492 /**
493 * Gets the SQL parameters for the corresponding SQL statement to insert the given
494 * element of the given collection into the database.
495 *
496 * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql.
497 *
498 * @return mixed[]
499 * @psalm-return list<mixed>
500 */
501 protected function getInsertRowSQLParameters(PersistentCollection $collection, object $element): array
502 {
503 return $this->collectJoinTableColumnParameters($collection, $element);
504 }
505
506 /**
507 * Collects the parameters for inserting/deleting on the join table in the order
508 * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
509 *
510 * @return mixed[]
511 * @psalm-return list<mixed>
512 */
513 private function collectJoinTableColumnParameters(
514 PersistentCollection $collection,
515 object $element,
516 ): array {
517 $params = [];
518 $mapping = $this->getMapping($collection);
519 assert($mapping->isManyToManyOwningSide());
520 $isComposite = count($mapping->joinTableColumns) > 2;
521
522 $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner());
523 $identifier2 = $this->uow->getEntityIdentifier($element);
524
525 $class1 = $class2 = null;
526 if ($isComposite) {
527 $class1 = $this->em->getClassMetadata($collection->getOwner()::class);
528 $class2 = $collection->getTypeClass();
529 }
530
531 foreach ($mapping->joinTableColumns as $joinTableColumn) {
532 $isRelationToSource = isset($mapping->relationToSourceKeyColumns[$joinTableColumn]);
533
534 if (! $isComposite) {
535 $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
536
537 continue;
538 }
539
540 if ($isRelationToSource) {
541 $params[] = $identifier1[$class1->getFieldForColumn($mapping->relationToSourceKeyColumns[$joinTableColumn])];
542
543 continue;
544 }
545
546 $params[] = $identifier2[$class2->getFieldForColumn($mapping->relationToTargetKeyColumns[$joinTableColumn])];
547 }
548
549 return $params;
550 }
551
552 /**
553 * @param bool $addFilters Whether the filter SQL should be included or not.
554 *
555 * @return mixed[] ordered vector:
556 * - quoted join table name
557 * - where clauses to be added for filtering
558 * - parameters to be bound for filtering
559 * - types of the parameters to be bound for filtering
560 * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>}
561 */
562 private function getJoinTableRestrictionsWithKey(
563 PersistentCollection $collection,
564 string $key,
565 bool $addFilters,
566 ): array {
567 $filterMapping = $this->getMapping($collection);
568 $mapping = $filterMapping;
569 $indexBy = $mapping->indexBy();
570 $id = $this->uow->getEntityIdentifier($collection->getOwner());
571 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
572 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
573
574 if (! $mapping->isOwningSide()) {
575 assert($mapping instanceof InverseSideMapping);
576 $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity);
577 $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy];
578 assert($mapping->isManyToManyOwningSide());
579 $joinColumns = $mapping->joinTable->joinColumns;
580 $sourceRelationMode = 'relationToTargetKeyColumns';
581 $targetRelationMode = 'relationToSourceKeyColumns';
582 } else {
583 assert($mapping->isManyToManyOwningSide());
584 $associationSourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
585 $joinColumns = $mapping->joinTable->inverseJoinColumns;
586 $sourceRelationMode = 'relationToSourceKeyColumns';
587 $targetRelationMode = 'relationToTargetKeyColumns';
588 }
589
590 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform) . ' t';
591 $whereClauses = [];
592 $params = [];
593 $types = [];
594
595 $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true);
596
597 if ($joinNeeded) { // extra join needed if indexBy is not a @id
598 $joinConditions = [];
599
600 foreach ($joinColumns as $joinTableColumn) {
601 $joinConditions[] = 't.' . $joinTableColumn->name . ' = tr.' . $joinTableColumn->referencedColumnName;
602 }
603
604 $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
605 $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
606 $columnName = $targetClass->getColumnName($indexBy);
607
608 $whereClauses[] = 'tr.' . $columnName . ' = ?';
609 $params[] = $key;
610 $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em);
611 }
612
613 foreach ($mapping->joinTableColumns as $joinTableColumn) {
614 if (isset($mapping->{$sourceRelationMode}[$joinTableColumn])) {
615 $column = $mapping->{$sourceRelationMode}[$joinTableColumn];
616 $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
617 $params[] = $sourceClass->containsForeignIdentifier
618 ? $id[$sourceClass->getFieldForColumn($column)]
619 : $id[$sourceClass->fieldNames[$column]];
620 $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em);
621 } elseif (! $joinNeeded) {
622 $column = $mapping->{$targetRelationMode}[$joinTableColumn];
623
624 $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
625 $params[] = $key;
626 $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em);
627 }
628 }
629
630 if ($addFilters) {
631 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping);
632
633 if ($filterSql) {
634 $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
635 $whereClauses[] = $filterSql;
636 }
637 }
638
639 return [$quotedJoinTable, $whereClauses, $params, $types];
640 }
641
642 /**
643 * @param bool $addFilters Whether the filter SQL should be included or not.
644 *
645 * @return mixed[] ordered vector:
646 * - quoted join table name
647 * - where clauses to be added for filtering
648 * - parameters to be bound for filtering
649 * - types of the parameters to be bound for filtering
650 * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>}
651 */
652 private function getJoinTableRestrictions(
653 PersistentCollection $collection,
654 object $element,
655 bool $addFilters,
656 ): array {
657 $filterMapping = $this->getMapping($collection);
658 $mapping = $filterMapping;
659
660 if (! $mapping->isOwningSide()) {
661 $sourceClass = $this->em->getClassMetadata($mapping->targetEntity);
662 $targetClass = $this->em->getClassMetadata($mapping->sourceEntity);
663 $sourceId = $this->uow->getEntityIdentifier($element);
664 $targetId = $this->uow->getEntityIdentifier($collection->getOwner());
665 } else {
666 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
667 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
668 $sourceId = $this->uow->getEntityIdentifier($collection->getOwner());
669 $targetId = $this->uow->getEntityIdentifier($element);
670 }
671
672 $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping);
673
674 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
675 $whereClauses = [];
676 $params = [];
677 $types = [];
678
679 foreach ($mapping->joinTableColumns as $joinTableColumn) {
680 $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?';
681
682 if (isset($mapping->relationToTargetKeyColumns[$joinTableColumn])) {
683 $targetColumn = $mapping->relationToTargetKeyColumns[$joinTableColumn];
684 $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)];
685 $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
686
687 continue;
688 }
689
690 // relationToSourceKeyColumns
691 $targetColumn = $mapping->relationToSourceKeyColumns[$joinTableColumn];
692 $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)];
693 $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em);
694 }
695
696 if ($addFilters) {
697 $quotedJoinTable .= ' t';
698
699 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping);
700
701 if ($filterSql) {
702 $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
703 $whereClauses[] = $filterSql;
704 }
705 }
706
707 return [$quotedJoinTable, $whereClauses, $params, $types];
708 }
709
710 /**
711 * Expands Criteria Parameters by walking the expressions and grabbing all
712 * parameters and types from it.
713 *
714 * @return mixed[][]
715 */
716 private function expandCriteriaParameters(Criteria $criteria): array
717 {
718 $expression = $criteria->getWhereExpression();
719
720 if ($expression === null) {
721 return [];
722 }
723
724 $valueVisitor = new SqlValueVisitor();
725
726 $valueVisitor->dispatch($expression);
727
728 [, $types] = $valueVisitor->getParamsAndTypes();
729
730 return $types;
731 }
732
733 private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string
734 {
735 $orderings = $criteria->orderings();
736 if ($orderings) {
737 $orderBy = [];
738 foreach ($orderings as $name => $direction) {
739 $field = $this->quoteStrategy->getColumnName(
740 $name,
741 $targetClass,
742 $this->platform,
743 );
744 $orderBy[] = $field . ' ' . $direction->value;
745 }
746
747 return ' ORDER BY ' . implode(', ', $orderBy);
748 }
749
750 return '';
751 }
752
753 /** @throws DBALException */
754 private function getLimitSql(Criteria $criteria): string
755 {
756 $limit = $criteria->getMaxResults();
757 $offset = $criteria->getFirstResult();
758
759 return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0);
760 }
761
762 private function getMapping(PersistentCollection $collection): AssociationMapping&ManyToManyAssociationMapping
763 {
764 $mapping = $collection->getMapping();
765
766 assert($mapping instanceof ManyToManyAssociationMapping);
767
768 return $mapping;
769 }
770}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php
new file mode 100644
index 0000000..0727b1f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php
@@ -0,0 +1,264 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use BadMethodCallException;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\DBAL\Exception as DBALException;
10use Doctrine\DBAL\Types\Type;
11use Doctrine\ORM\EntityNotFoundException;
12use Doctrine\ORM\Mapping\MappingException;
13use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
14use Doctrine\ORM\PersistentCollection;
15use Doctrine\ORM\Utility\PersisterHelper;
16
17use function array_reverse;
18use function array_values;
19use function assert;
20use function implode;
21use function is_int;
22use function is_string;
23
24/**
25 * Persister for one-to-many collections.
26 */
27class OneToManyPersister extends AbstractCollectionPersister
28{
29 public function delete(PersistentCollection $collection): void
30 {
31 // The only valid case here is when you have weak entities. In this
32 // scenario, you have @OneToMany with orphanRemoval=true, and replacing
33 // the entire collection with a new would trigger this operation.
34 $mapping = $this->getMapping($collection);
35
36 if (! $mapping->orphanRemoval) {
37 // Handling non-orphan removal should never happen, as @OneToMany
38 // can only be inverse side. For owning side one to many, it is
39 // required to have a join table, which would classify as a ManyToManyPersister.
40 return;
41 }
42
43 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
44
45 $targetClass->isInheritanceTypeJoined()
46 ? $this->deleteJoinedEntityCollection($collection)
47 : $this->deleteEntityCollection($collection);
48 }
49
50 public function update(PersistentCollection $collection): void
51 {
52 // This can never happen. One to many can only be inverse side.
53 // For owning side one to many, it is required to have a join table,
54 // then classifying it as a ManyToManyPersister.
55 return;
56 }
57
58 public function get(PersistentCollection $collection, mixed $index): object|null
59 {
60 $mapping = $this->getMapping($collection);
61
62 if (! $mapping->isIndexed()) {
63 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
64 }
65
66 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
67
68 return $persister->load(
69 [
70 $mapping->mappedBy => $collection->getOwner(),
71 $mapping->indexBy() => $index,
72 ],
73 null,
74 $mapping,
75 [],
76 null,
77 1,
78 );
79 }
80
81 public function count(PersistentCollection $collection): int
82 {
83 $mapping = $this->getMapping($collection);
84 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
85
86 // only works with single id identifier entities. Will throw an
87 // exception in Entity Persisters if that is not the case for the
88 // 'mappedBy' field.
89 $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
90
91 return $persister->count($criteria);
92 }
93
94 /**
95 * {@inheritDoc}
96 */
97 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
98 {
99 $mapping = $this->getMapping($collection);
100 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
101
102 return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length);
103 }
104
105 public function containsKey(PersistentCollection $collection, mixed $key): bool
106 {
107 $mapping = $this->getMapping($collection);
108
109 if (! $mapping->isIndexed()) {
110 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
111 }
112
113 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
114
115 // only works with single id identifier entities. Will throw an
116 // exception in Entity Persisters if that is not the case for the
117 // 'mappedBy' field.
118 $criteria = new Criteria();
119
120 $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
121 $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key));
122
123 return (bool) $persister->count($criteria);
124 }
125
126 public function contains(PersistentCollection $collection, object $element): bool
127 {
128 if (! $this->isValidEntityState($element)) {
129 return false;
130 }
131
132 $mapping = $this->getMapping($collection);
133 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
134
135 // only works with single id identifier entities. Will throw an
136 // exception in Entity Persisters if that is not the case for the
137 // 'mappedBy' field.
138 $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
139
140 return $persister->exists($element, $criteria);
141 }
142
143 /**
144 * {@inheritDoc}
145 */
146 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
147 {
148 throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
149 }
150
151 /**
152 * @throws DBALException
153 * @throws EntityNotFoundException
154 * @throws MappingException
155 */
156 private function deleteEntityCollection(PersistentCollection $collection): int
157 {
158 $mapping = $this->getMapping($collection);
159 $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
160 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
161 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
162 $columns = [];
163 $parameters = [];
164 $types = [];
165
166 foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) {
167 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
168 $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)];
169 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em);
170 }
171
172 $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
173 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
174
175 if ($targetClass->isInheritanceTypeSingleTable()) {
176 $discriminatorColumn = $targetClass->getDiscriminatorColumn();
177 $statement .= ' AND ' . $discriminatorColumn->name . ' = ?';
178 $parameters[] = $targetClass->discriminatorValue;
179 $types[] = $discriminatorColumn->type;
180 }
181
182 $numAffected = $this->conn->executeStatement($statement, $parameters, $types);
183
184 assert(is_int($numAffected));
185
186 return $numAffected;
187 }
188
189 /**
190 * Delete Class Table Inheritance entities.
191 * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables.
192 *
193 * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =)
194 *
195 * @throws DBALException
196 */
197 private function deleteJoinedEntityCollection(PersistentCollection $collection): int
198 {
199 $mapping = $this->getMapping($collection);
200 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
201 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
202 $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName);
203
204 // 1) Build temporary table DDL
205 $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
206 $idColumnNames = $rootClass->getIdentifierColumnNames();
207 $idColumnList = implode(', ', $idColumnNames);
208 $columnDefinitions = [];
209
210 foreach ($idColumnNames as $idColumnName) {
211 $columnDefinitions[$idColumnName] = [
212 'name' => $idColumnName,
213 'notnull' => true,
214 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)),
215 ];
216 }
217
218 $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable
219 . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')';
220
221 $this->conn->executeStatement($statement);
222
223 // 2) Build insert table records into temporary table
224 $query = $this->em->createQuery(
225 ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames())
226 . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner',
227 )->setParameter('owner', $collection->getOwner());
228
229 $sql = $query->getSQL();
230 assert(is_string($sql));
231 $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql;
232 $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner()));
233 $numDeleted = $this->conn->executeStatement($statement, $parameters);
234
235 // 3) Delete records on each table in the hierarchy
236 $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses];
237
238 foreach (array_reverse($classNames) as $className) {
239 $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform);
240 $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')'
241 . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')';
242
243 $this->conn->executeStatement($statement);
244 }
245
246 // 4) Drop temporary table
247 $statement = $this->platform->getDropTemporaryTableSQL($tempTable);
248
249 $this->conn->executeStatement($statement);
250
251 assert(is_int($numDeleted));
252
253 return $numDeleted;
254 }
255
256 private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping
257 {
258 $mapping = $collection->getMapping();
259
260 assert($mapping->isOneToMany());
261
262 return $mapping;
263 }
264}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php
new file mode 100644
index 0000000..cf8a74e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php
@@ -0,0 +1,66 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\ORM\Mapping\ClassMetadata;
9
10use function sprintf;
11
12/**
13 * Base class for entity persisters that implement a certain inheritance mapping strategy.
14 * All these persisters are assumed to use a discriminator column to discriminate entity
15 * types in the hierarchy.
16 */
17abstract class AbstractEntityInheritancePersister extends BasicEntityPersister
18{
19 /**
20 * {@inheritDoc}
21 */
22 protected function prepareInsertData(object $entity): array
23 {
24 $data = parent::prepareInsertData($entity);
25
26 // Populate the discriminator column
27 $discColumn = $this->class->getDiscriminatorColumn();
28 $this->columnTypes[$discColumn->name] = $discColumn->type;
29 $data[$this->getDiscriminatorColumnTableName()][$discColumn->name] = $this->class->discriminatorValue;
30
31 return $data;
32 }
33
34 /**
35 * Gets the name of the table that contains the discriminator column.
36 */
37 abstract protected function getDiscriminatorColumnTableName(): string;
38
39 protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string
40 {
41 $tableAlias = $alias === 'r' ? '' : $alias;
42 $fieldMapping = $class->fieldMappings[$field];
43 $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
44 $sql = sprintf(
45 '%s.%s',
46 $this->getSQLTableAlias($class->name, $tableAlias),
47 $this->quoteStrategy->getColumnName($field, $class, $this->platform),
48 );
49
50 $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name);
51
52 $type = Type::getType($fieldMapping->type);
53 $sql = $type->convertToPHPValueSQL($sql, $this->platform);
54
55 return $sql . ' AS ' . $columnAlias;
56 }
57
58 protected function getSelectJoinColumnSQL(string $tableAlias, string $joinColumnName, string $quotedColumnName, string $type): string
59 {
60 $columnAlias = $this->getSQLColumnAlias($joinColumnName);
61
62 $this->currentPersisterContext->rsm->addMetaResult('r', $columnAlias, $joinColumnName, false, $type);
63
64 return $tableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
65 }
66}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php
new file mode 100644
index 0000000..377e03c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php
@@ -0,0 +1,2085 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use BackedEnum;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\Common\Collections\Expr\Comparison;
10use Doctrine\Common\Collections\Order;
11use Doctrine\DBAL\ArrayParameterType;
12use Doctrine\DBAL\Connection;
13use Doctrine\DBAL\LockMode;
14use Doctrine\DBAL\ParameterType;
15use Doctrine\DBAL\Platforms\AbstractPlatform;
16use Doctrine\DBAL\Result;
17use Doctrine\DBAL\Types\Type;
18use Doctrine\DBAL\Types\Types;
19use Doctrine\ORM\EntityManagerInterface;
20use Doctrine\ORM\Mapping\AssociationMapping;
21use Doctrine\ORM\Mapping\ClassMetadata;
22use Doctrine\ORM\Mapping\JoinColumnMapping;
23use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
24use Doctrine\ORM\Mapping\MappingException;
25use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
26use Doctrine\ORM\Mapping\QuoteStrategy;
27use Doctrine\ORM\OptimisticLockException;
28use Doctrine\ORM\PersistentCollection;
29use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
30use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
31use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
32use Doctrine\ORM\Persisters\SqlExpressionVisitor;
33use Doctrine\ORM\Persisters\SqlValueVisitor;
34use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
35use Doctrine\ORM\Query;
36use Doctrine\ORM\Query\QueryException;
37use Doctrine\ORM\Query\ResultSetMapping;
38use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
39use Doctrine\ORM\UnitOfWork;
40use Doctrine\ORM\Utility\IdentifierFlattener;
41use Doctrine\ORM\Utility\LockSqlHelper;
42use Doctrine\ORM\Utility\PersisterHelper;
43use LengthException;
44
45use function array_combine;
46use function array_keys;
47use function array_map;
48use function array_merge;
49use function array_search;
50use function array_unique;
51use function array_values;
52use function assert;
53use function count;
54use function implode;
55use function is_array;
56use function is_object;
57use function reset;
58use function spl_object_id;
59use function sprintf;
60use function str_contains;
61use function strtoupper;
62use function trim;
63
64/**
65 * A BasicEntityPersister maps an entity to a single table in a relational database.
66 *
67 * A persister is always responsible for a single entity type.
68 *
69 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
70 * state of entities onto a relational database when the UnitOfWork is committed,
71 * as well as for basic querying of entities and their associations (not DQL).
72 *
73 * The persisting operations that are invoked during a commit of a UnitOfWork to
74 * persist the persistent entity state are:
75 *
76 * - {@link addInsert} : To schedule an entity for insertion.
77 * - {@link executeInserts} : To execute all scheduled insertions.
78 * - {@link update} : To update the persistent state of an entity.
79 * - {@link delete} : To delete the persistent state of an entity.
80 *
81 * As can be seen from the above list, insertions are batched and executed all at once
82 * for increased efficiency.
83 *
84 * The querying operations invoked during a UnitOfWork, either through direct find
85 * requests or lazy-loading, are the following:
86 *
87 * - {@link load} : Loads (the state of) a single, managed entity.
88 * - {@link loadAll} : Loads multiple, managed entities.
89 * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
90 * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
91 * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
92 *
93 * The BasicEntityPersister implementation provides the default behavior for
94 * persisting and querying entities that are mapped to a single database table.
95 *
96 * Subclasses can be created to provide custom persisting and querying strategies,
97 * i.e. spanning multiple tables.
98 */
99class BasicEntityPersister implements EntityPersister
100{
101 use LockSqlHelper;
102
103 /** @var array<string,string> */
104 private static array $comparisonMap = [
105 Comparison::EQ => '= %s',
106 Comparison::NEQ => '!= %s',
107 Comparison::GT => '> %s',
108 Comparison::GTE => '>= %s',
109 Comparison::LT => '< %s',
110 Comparison::LTE => '<= %s',
111 Comparison::IN => 'IN (%s)',
112 Comparison::NIN => 'NOT IN (%s)',
113 Comparison::CONTAINS => 'LIKE %s',
114 Comparison::STARTS_WITH => 'LIKE %s',
115 Comparison::ENDS_WITH => 'LIKE %s',
116 ];
117
118 /**
119 * The underlying DBAL Connection of the used EntityManager.
120 */
121 protected Connection $conn;
122
123 /**
124 * The database platform.
125 */
126 protected AbstractPlatform $platform;
127
128 /**
129 * Queued inserts.
130 *
131 * @psalm-var array<int, object>
132 */
133 protected array $queuedInserts = [];
134
135 /**
136 * The map of column names to DBAL mapping types of all prepared columns used
137 * when INSERTing or UPDATEing an entity.
138 *
139 * @see prepareInsertData($entity)
140 * @see prepareUpdateData($entity)
141 *
142 * @var mixed[]
143 */
144 protected array $columnTypes = [];
145
146 /**
147 * The map of quoted column names.
148 *
149 * @see prepareInsertData($entity)
150 * @see prepareUpdateData($entity)
151 *
152 * @var mixed[]
153 */
154 protected array $quotedColumns = [];
155
156 /**
157 * The INSERT SQL statement used for entities handled by this persister.
158 * This SQL is only generated once per request, if at all.
159 */
160 private string|null $insertSql = null;
161
162 /**
163 * The quote strategy.
164 */
165 protected QuoteStrategy $quoteStrategy;
166
167 /**
168 * The IdentifierFlattener used for manipulating identifiers
169 */
170 protected readonly IdentifierFlattener $identifierFlattener;
171
172 protected CachedPersisterContext $currentPersisterContext;
173 private readonly CachedPersisterContext $limitsHandlingContext;
174 private readonly CachedPersisterContext $noLimitsContext;
175
176 /**
177 * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
178 * and persists instances of the class described by the given ClassMetadata descriptor.
179 *
180 * @param ClassMetadata $class Metadata object that describes the mapping of the mapped entity class.
181 */
182 public function __construct(
183 protected EntityManagerInterface $em,
184 protected ClassMetadata $class,
185 ) {
186 $this->conn = $em->getConnection();
187 $this->platform = $this->conn->getDatabasePlatform();
188 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
189 $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
190 $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
191 $class,
192 new Query\ResultSetMapping(),
193 false,
194 );
195 $this->limitsHandlingContext = new CachedPersisterContext(
196 $class,
197 new Query\ResultSetMapping(),
198 true,
199 );
200 }
201
202 public function getClassMetadata(): ClassMetadata
203 {
204 return $this->class;
205 }
206
207 public function getResultSetMapping(): ResultSetMapping
208 {
209 return $this->currentPersisterContext->rsm;
210 }
211
212 public function addInsert(object $entity): void
213 {
214 $this->queuedInserts[spl_object_id($entity)] = $entity;
215 }
216
217 /**
218 * {@inheritDoc}
219 */
220 public function getInserts(): array
221 {
222 return $this->queuedInserts;
223 }
224
225 public function executeInserts(): void
226 {
227 if (! $this->queuedInserts) {
228 return;
229 }
230
231 $uow = $this->em->getUnitOfWork();
232 $idGenerator = $this->class->idGenerator;
233 $isPostInsertId = $idGenerator->isPostInsertGenerator();
234
235 $stmt = $this->conn->prepare($this->getInsertSQL());
236 $tableName = $this->class->getTableName();
237
238 foreach ($this->queuedInserts as $key => $entity) {
239 $insertData = $this->prepareInsertData($entity);
240
241 if (isset($insertData[$tableName])) {
242 $paramIndex = 1;
243
244 foreach ($insertData[$tableName] as $column => $value) {
245 $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
246 }
247 }
248
249 $stmt->executeStatement();
250
251 if ($isPostInsertId) {
252 $generatedId = $idGenerator->generateId($this->em, $entity);
253 $id = [$this->class->identifier[0] => $generatedId];
254
255 $uow->assignPostInsertId($entity, $generatedId);
256 } else {
257 $id = $this->class->getIdentifierValues($entity);
258 }
259
260 if ($this->class->requiresFetchAfterChange) {
261 $this->assignDefaultVersionAndUpsertableValues($entity, $id);
262 }
263
264 // Unset this queued insert, so that the prepareUpdateData() method knows right away
265 // (for the next entity already) that the current entity has been written to the database
266 // and no extra updates need to be scheduled to refer to it.
267 //
268 // In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
269 // from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
270 // were given to our addInsert() method.
271 unset($this->queuedInserts[$key]);
272 }
273 }
274
275 /**
276 * Retrieves the default version value which was created
277 * by the preceding INSERT statement and assigns it back in to the
278 * entities version field if the given entity is versioned.
279 * Also retrieves values of columns marked as 'non insertable' and / or
280 * 'not updatable' and assigns them back to the entities corresponding fields.
281 *
282 * @param mixed[] $id
283 */
284 protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void
285 {
286 $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);
287
288 foreach ($values as $field => $value) {
289 $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform);
290
291 $this->class->setFieldValue($entity, $field, $value);
292 }
293 }
294
295 /**
296 * Fetches the current version value of a versioned entity and / or the values of fields
297 * marked as 'not insertable' and / or 'not updatable'.
298 *
299 * @param mixed[] $id
300 */
301 protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed
302 {
303 $columnNames = [];
304 foreach ($this->class->fieldMappings as $key => $column) {
305 if (isset($column->generated) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {
306 $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);
307 }
308 }
309
310 $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
311 $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
312
313 // FIXME: Order with composite keys might not be correct
314 $sql = 'SELECT ' . implode(', ', $columnNames)
315 . ' FROM ' . $tableName
316 . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
317
318 $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
319
320 $values = $this->conn->fetchNumeric(
321 $sql,
322 array_values($flatId),
323 $this->extractIdentifierTypes($id, $versionedClass),
324 );
325
326 if ($values === false) {
327 throw new LengthException('Unexpected empty result for database query.');
328 }
329
330 $values = array_combine(array_keys($columnNames), $values);
331
332 if (! $values) {
333 throw new LengthException('Unexpected number of database columns.');
334 }
335
336 return $values;
337 }
338
339 /**
340 * @param mixed[] $id
341 *
342 * @return list<ParameterType|int|string>
343 * @psalm-return list<ParameterType::*|ArrayParameterType::*|string>
344 */
345 final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
346 {
347 $types = [];
348
349 foreach ($id as $field => $value) {
350 $types = [...$types, ...$this->getTypes($field, $value, $versionedClass)];
351 }
352
353 return $types;
354 }
355
356 public function update(object $entity): void
357 {
358 $tableName = $this->class->getTableName();
359 $updateData = $this->prepareUpdateData($entity);
360
361 if (! isset($updateData[$tableName])) {
362 return;
363 }
364
365 $data = $updateData[$tableName];
366
367 if (! $data) {
368 return;
369 }
370
371 $isVersioned = $this->class->isVersioned;
372 $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
373
374 $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
375
376 if ($this->class->requiresFetchAfterChange) {
377 $id = $this->class->getIdentifierValues($entity);
378
379 $this->assignDefaultVersionAndUpsertableValues($entity, $id);
380 }
381 }
382
383 /**
384 * Performs an UPDATE statement for an entity on a specific table.
385 * The UPDATE can optionally be versioned, which requires the entity to have a version field.
386 *
387 * @param object $entity The entity object being updated.
388 * @param string $quotedTableName The quoted name of the table to apply the UPDATE on.
389 * @param mixed[] $updateData The map of columns to update (column => value).
390 * @param bool $versioned Whether the UPDATE should be versioned.
391 *
392 * @throws UnrecognizedField
393 * @throws OptimisticLockException
394 */
395 final protected function updateTable(
396 object $entity,
397 string $quotedTableName,
398 array $updateData,
399 bool $versioned = false,
400 ): void {
401 $set = [];
402 $types = [];
403 $params = [];
404
405 foreach ($updateData as $columnName => $value) {
406 $placeholder = '?';
407 $column = $columnName;
408
409 switch (true) {
410 case isset($this->class->fieldNames[$columnName]):
411 $fieldName = $this->class->fieldNames[$columnName];
412 $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
413
414 if (isset($this->class->fieldMappings[$fieldName])) {
415 $type = Type::getType($this->columnTypes[$columnName]);
416 $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
417 }
418
419 break;
420
421 case isset($this->quotedColumns[$columnName]):
422 $column = $this->quotedColumns[$columnName];
423
424 break;
425 }
426
427 $params[] = $value;
428 $set[] = $column . ' = ' . $placeholder;
429 $types[] = $this->columnTypes[$columnName];
430 }
431
432 $where = [];
433 $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
434
435 foreach ($this->class->identifier as $idField) {
436 if (! isset($this->class->associationMappings[$idField])) {
437 $params[] = $identifier[$idField];
438 $types[] = $this->class->fieldMappings[$idField]->type;
439 $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
440
441 continue;
442 }
443
444 assert($this->class->associationMappings[$idField]->isToOneOwningSide());
445
446 $params[] = $identifier[$idField];
447 $where[] = $this->quoteStrategy->getJoinColumnName(
448 $this->class->associationMappings[$idField]->joinColumns[0],
449 $this->class,
450 $this->platform,
451 );
452
453 $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]->targetEntity);
454 $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
455
456 if ($targetType === []) {
457 throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]);
458 }
459
460 $types[] = reset($targetType);
461 }
462
463 if ($versioned) {
464 $versionField = $this->class->versionField;
465 assert($versionField !== null);
466 $versionFieldType = $this->class->fieldMappings[$versionField]->type;
467 $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
468
469 $where[] = $versionColumn;
470 $types[] = $this->class->fieldMappings[$versionField]->type;
471 $params[] = $this->class->reflFields[$versionField]->getValue($entity);
472
473 switch ($versionFieldType) {
474 case Types::SMALLINT:
475 case Types::INTEGER:
476 case Types::BIGINT:
477 $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
478 break;
479
480 case Types::DATETIME_MUTABLE:
481 $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
482 break;
483 }
484 }
485
486 $sql = 'UPDATE ' . $quotedTableName
487 . ' SET ' . implode(', ', $set)
488 . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
489
490 $result = $this->conn->executeStatement($sql, $params, $types);
491
492 if ($versioned && ! $result) {
493 throw OptimisticLockException::lockFailed($entity);
494 }
495 }
496
497 /**
498 * @param array<mixed> $identifier
499 * @param string[] $types
500 *
501 * @todo Add check for platform if it supports foreign keys/cascading.
502 */
503 protected function deleteJoinTableRecords(array $identifier, array $types): void
504 {
505 foreach ($this->class->associationMappings as $mapping) {
506 if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) {
507 continue;
508 }
509
510 // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
511 // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
512 $selfReferential = ($mapping->targetEntity === $mapping->sourceEntity);
513 $class = $this->class;
514 $association = $mapping;
515 $otherColumns = [];
516 $otherKeys = [];
517 $keys = [];
518
519 if (! $mapping->isOwningSide()) {
520 $class = $this->em->getClassMetadata($mapping->targetEntity);
521 }
522
523 $association = $this->em->getMetadataFactory()->getOwningSide($association);
524 $joinColumns = $mapping->isOwningSide()
525 ? $association->joinTable->joinColumns
526 : $association->joinTable->inverseJoinColumns;
527
528 if ($selfReferential) {
529 $otherColumns = ! $mapping->isOwningSide()
530 ? $association->joinTable->joinColumns
531 : $association->joinTable->inverseJoinColumns;
532 }
533
534 foreach ($joinColumns as $joinColumn) {
535 $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
536 }
537
538 foreach ($otherColumns as $joinColumn) {
539 $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
540 }
541
542 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
543
544 $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
545
546 if ($selfReferential) {
547 $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types);
548 }
549 }
550 }
551
552 public function delete(object $entity): bool
553 {
554 $class = $this->class;
555 $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
556 $tableName = $this->quoteStrategy->getTableName($class, $this->platform);
557 $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
558 $id = array_combine($idColumns, $identifier);
559 $types = $this->getClassIdentifiersTypes($class);
560
561 $this->deleteJoinTableRecords($identifier, $types);
562
563 return (bool) $this->conn->delete($tableName, $id, $types);
564 }
565
566 /**
567 * Prepares the changeset of an entity for database insertion (UPDATE).
568 *
569 * The changeset is obtained from the currently running UnitOfWork.
570 *
571 * During this preparation the array that is passed as the second parameter is filled with
572 * <columnName> => <value> pairs, grouped by table name.
573 *
574 * Example:
575 * <code>
576 * array(
577 * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
578 * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
579 * ...
580 * )
581 * </code>
582 *
583 * @param object $entity The entity for which to prepare the data.
584 * @param bool $isInsert Whether the data to be prepared refers to an insert statement.
585 *
586 * @return mixed[][] The prepared data.
587 * @psalm-return array<string, array<array-key, mixed|null>>
588 */
589 protected function prepareUpdateData(object $entity, bool $isInsert = false): array
590 {
591 $versionField = null;
592 $result = [];
593 $uow = $this->em->getUnitOfWork();
594
595 $versioned = $this->class->isVersioned;
596 if ($versioned !== false) {
597 $versionField = $this->class->versionField;
598 }
599
600 foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
601 if (isset($versionField) && $versionField === $field) {
602 continue;
603 }
604
605 if (isset($this->class->embeddedClasses[$field])) {
606 continue;
607 }
608
609 $newVal = $change[1];
610
611 if (! isset($this->class->associationMappings[$field])) {
612 $fieldMapping = $this->class->fieldMappings[$field];
613 $columnName = $fieldMapping->columnName;
614
615 if (! $isInsert && isset($fieldMapping->notUpdatable)) {
616 continue;
617 }
618
619 if ($isInsert && isset($fieldMapping->notInsertable)) {
620 continue;
621 }
622
623 $this->columnTypes[$columnName] = $fieldMapping->type;
624
625 $result[$this->getOwningTable($field)][$columnName] = $newVal;
626
627 continue;
628 }
629
630 $assoc = $this->class->associationMappings[$field];
631
632 // Only owning side of x-1 associations can have a FK column.
633 if (! $assoc->isToOneOwningSide()) {
634 continue;
635 }
636
637 if ($newVal !== null) {
638 $oid = spl_object_id($newVal);
639
640 // If the associated entity $newVal is not yet persisted and/or does not yet have
641 // an ID assigned, we must set $newVal = null. This will insert a null value and
642 // schedule an extra update on the UnitOfWork.
643 //
644 // This gives us extra time to a) possibly obtain a database-generated identifier
645 // value for $newVal, and b) insert $newVal into the database before the foreign
646 // key reference is being made.
647 //
648 // When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware
649 // of the implementation details that our own executeInserts() method will remove
650 // entities from the former as soon as the insert statement has been executed and
651 // a post-insert ID has been assigned (if necessary), and that the UnitOfWork has
652 // already removed entities from its own list at the time they were passed to our
653 // addInsert() method.
654 //
655 // Then, there is one extra exception we can make: An entity that references back to itself
656 // _and_ uses an application-provided ID (the "NONE" generator strategy) also does not
657 // need the extra update, although it is still in the list of insertions itself.
658 // This looks like a minor optimization at first, but is the capstone for being able to
659 // use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs).
660 if (
661 (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal))
662 && ! ($newVal === $entity && $this->class->isIdentifierNatural())
663 ) {
664 $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
665
666 $newVal = null;
667 }
668 }
669
670 $newValId = null;
671
672 if ($newVal !== null) {
673 $newValId = $uow->getEntityIdentifier($newVal);
674 }
675
676 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
677 $owningTable = $this->getOwningTable($field);
678
679 foreach ($assoc->joinColumns as $joinColumn) {
680 $sourceColumn = $joinColumn->name;
681 $targetColumn = $joinColumn->referencedColumnName;
682 $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
683
684 $this->quotedColumns[$sourceColumn] = $quotedColumn;
685 $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
686 $result[$owningTable][$sourceColumn] = $newValId
687 ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
688 : null;
689 }
690 }
691
692 return $result;
693 }
694
695 /**
696 * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
697 * The changeset of the entity is obtained from the currently running UnitOfWork.
698 *
699 * The default insert data preparation is the same as for updates.
700 *
701 * @see prepareUpdateData
702 *
703 * @param object $entity The entity for which to prepare the data.
704 *
705 * @return mixed[][] The prepared data for the tables to update.
706 * @psalm-return array<string, mixed[]>
707 */
708 protected function prepareInsertData(object $entity): array
709 {
710 return $this->prepareUpdateData($entity, true);
711 }
712
713 public function getOwningTable(string $fieldName): string
714 {
715 return $this->class->getTableName();
716 }
717
718 /**
719 * {@inheritDoc}
720 */
721 public function load(
722 array $criteria,
723 object|null $entity = null,
724 AssociationMapping|null $assoc = null,
725 array $hints = [],
726 LockMode|int|null $lockMode = null,
727 int|null $limit = null,
728 array|null $orderBy = null,
729 ): object|null {
730 $this->switchPersisterContext(null, $limit);
731
732 $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
733 [$params, $types] = $this->expandParameters($criteria);
734 $stmt = $this->conn->executeQuery($sql, $params, $types);
735
736 if ($entity !== null) {
737 $hints[Query::HINT_REFRESH] = true;
738 $hints[Query::HINT_REFRESH_ENTITY] = $entity;
739 }
740
741 $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
742 $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
743
744 return $entities ? $entities[0] : null;
745 }
746
747 /**
748 * {@inheritDoc}
749 */
750 public function loadById(array $identifier, object|null $entity = null): object|null
751 {
752 return $this->load($identifier, $entity);
753 }
754
755 /**
756 * {@inheritDoc}
757 */
758 public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null
759 {
760 $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc->targetEntity);
761 if ($foundEntity !== false) {
762 return $foundEntity;
763 }
764
765 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
766
767 if ($assoc->isOwningSide()) {
768 $isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy);
769
770 // Mark inverse side as fetched in the hints, otherwise the UoW would
771 // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
772 $hints = [];
773
774 if ($isInverseSingleValued) {
775 $hints['fetched']['r'][$assoc->inversedBy] = true;
776 }
777
778 $targetEntity = $this->load($identifier, null, $assoc, $hints);
779
780 // Complete bidirectional association, if necessary
781 if ($targetEntity !== null && $isInverseSingleValued) {
782 $targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity);
783 }
784
785 return $targetEntity;
786 }
787
788 assert(isset($assoc->mappedBy));
789 $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity);
790 $owningAssoc = $targetClass->getAssociationMapping($assoc->mappedBy);
791 assert($owningAssoc->isOneToOneOwningSide());
792
793 $computedIdentifier = [];
794
795 // TRICKY: since the association is specular source and target are flipped
796 foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
797 if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
798 throw MappingException::joinColumnMustPointToMappedField(
799 $sourceClass->name,
800 $sourceKeyColumn,
801 );
802 }
803
804 $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
805 $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
806 }
807
808 $targetEntity = $this->load($computedIdentifier, null, $assoc);
809
810 if ($targetEntity !== null) {
811 $targetClass->setFieldValue($targetEntity, $assoc->mappedBy, $sourceEntity);
812 }
813
814 return $targetEntity;
815 }
816
817 /**
818 * {@inheritDoc}
819 */
820 public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void
821 {
822 $sql = $this->getSelectSQL($id, null, $lockMode);
823 [$params, $types] = $this->expandParameters($id);
824 $stmt = $this->conn->executeQuery($sql, $params, $types);
825
826 $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
827 $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
828 }
829
830 public function count(array|Criteria $criteria = []): int
831 {
832 $sql = $this->getCountSQL($criteria);
833
834 [$params, $types] = $criteria instanceof Criteria
835 ? $this->expandCriteriaParameters($criteria)
836 : $this->expandParameters($criteria);
837
838 return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
839 }
840
841 /**
842 * {@inheritDoc}
843 */
844 public function loadCriteria(Criteria $criteria): array
845 {
846 $orderBy = array_map(
847 static fn (Order $order): string => $order->value,
848 $criteria->orderings(),
849 );
850 $limit = $criteria->getMaxResults();
851 $offset = $criteria->getFirstResult();
852 $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
853
854 [$params, $types] = $this->expandCriteriaParameters($criteria);
855
856 $stmt = $this->conn->executeQuery($query, $params, $types);
857 $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
858
859 return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
860 }
861
862 /**
863 * {@inheritDoc}
864 */
865 public function expandCriteriaParameters(Criteria $criteria): array
866 {
867 $expression = $criteria->getWhereExpression();
868 $sqlParams = [];
869 $sqlTypes = [];
870
871 if ($expression === null) {
872 return [$sqlParams, $sqlTypes];
873 }
874
875 $valueVisitor = new SqlValueVisitor();
876
877 $valueVisitor->dispatch($expression);
878
879 [, $types] = $valueVisitor->getParamsAndTypes();
880
881 foreach ($types as $type) {
882 [$field, $value, $operator] = $type;
883
884 if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
885 continue;
886 }
887
888 $sqlParams = [...$sqlParams, ...$this->getValues($value)];
889 $sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)];
890 }
891
892 return [$sqlParams, $sqlTypes];
893 }
894
895 /**
896 * {@inheritDoc}
897 */
898 public function loadAll(
899 array $criteria = [],
900 array|null $orderBy = null,
901 int|null $limit = null,
902 int|null $offset = null,
903 ): array {
904 $this->switchPersisterContext($offset, $limit);
905
906 $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
907 [$params, $types] = $this->expandParameters($criteria);
908 $stmt = $this->conn->executeQuery($sql, $params, $types);
909
910 $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
911
912 return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
913 }
914
915 /**
916 * {@inheritDoc}
917 */
918 public function getManyToManyCollection(
919 AssociationMapping $assoc,
920 object $sourceEntity,
921 int|null $offset = null,
922 int|null $limit = null,
923 ): array {
924 assert($assoc->isManyToMany());
925 $this->switchPersisterContext($offset, $limit);
926
927 $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
928
929 return $this->loadArrayFromResult($assoc, $stmt);
930 }
931
932 /**
933 * Loads an array of entities from a given DBAL statement.
934 *
935 * @return mixed[]
936 */
937 private function loadArrayFromResult(AssociationMapping $assoc, Result $stmt): array
938 {
939 $rsm = $this->currentPersisterContext->rsm;
940 $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
941
942 if ($assoc->isIndexed()) {
943 $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
944 $rsm->addIndexBy('r', $assoc->indexBy());
945 }
946
947 return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
948 }
949
950 /**
951 * Hydrates a collection from a given DBAL statement.
952 *
953 * @return mixed[]
954 */
955 private function loadCollectionFromStatement(
956 AssociationMapping $assoc,
957 Result $stmt,
958 PersistentCollection $coll,
959 ): array {
960 $rsm = $this->currentPersisterContext->rsm;
961 $hints = [
962 UnitOfWork::HINT_DEFEREAGERLOAD => true,
963 'collection' => $coll,
964 ];
965
966 if ($assoc->isIndexed()) {
967 $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
968 $rsm->addIndexBy('r', $assoc->indexBy());
969 }
970
971 return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
972 }
973
974 /**
975 * {@inheritDoc}
976 */
977 public function loadManyToManyCollection(AssociationMapping $assoc, object $sourceEntity, PersistentCollection $collection): array
978 {
979 assert($assoc->isManyToMany());
980 $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
981
982 return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
983 }
984
985 /** @throws MappingException */
986 private function getManyToManyStatement(
987 AssociationMapping&ManyToManyAssociationMapping $assoc,
988 object $sourceEntity,
989 int|null $offset = null,
990 int|null $limit = null,
991 ): Result {
992 $this->switchPersisterContext($offset, $limit);
993
994 $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity);
995 $class = $sourceClass;
996 $association = $assoc;
997 $criteria = [];
998 $parameters = [];
999
1000 if (! $assoc->isOwningSide()) {
1001 $class = $this->em->getClassMetadata($assoc->targetEntity);
1002 }
1003
1004 $association = $this->em->getMetadataFactory()->getOwningSide($assoc);
1005 $joinColumns = $assoc->isOwningSide()
1006 ? $association->joinTable->joinColumns
1007 : $association->joinTable->inverseJoinColumns;
1008
1009 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1010
1011 foreach ($joinColumns as $joinColumn) {
1012 $sourceKeyColumn = $joinColumn->referencedColumnName;
1013 $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1014
1015 switch (true) {
1016 case $sourceClass->containsForeignIdentifier:
1017 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1018 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1019
1020 if (isset($sourceClass->associationMappings[$field])) {
1021 $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1022 $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]];
1023 }
1024
1025 break;
1026
1027 case isset($sourceClass->fieldNames[$sourceKeyColumn]):
1028 $field = $sourceClass->fieldNames[$sourceKeyColumn];
1029 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1030
1031 break;
1032
1033 default:
1034 throw MappingException::joinColumnMustPointToMappedField(
1035 $sourceClass->name,
1036 $sourceKeyColumn,
1037 );
1038 }
1039
1040 $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
1041 $parameters[] = [
1042 'value' => $value,
1043 'field' => $field,
1044 'class' => $sourceClass,
1045 ];
1046 }
1047
1048 $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1049 [$params, $types] = $this->expandToManyParameters($parameters);
1050
1051 return $this->conn->executeQuery($sql, $params, $types);
1052 }
1053
1054 public function getSelectSQL(
1055 array|Criteria $criteria,
1056 AssociationMapping|null $assoc = null,
1057 LockMode|int|null $lockMode = null,
1058 int|null $limit = null,
1059 int|null $offset = null,
1060 array|null $orderBy = null,
1061 ): string {
1062 $this->switchPersisterContext($offset, $limit);
1063
1064 $joinSql = '';
1065 $orderBySql = '';
1066
1067 if ($assoc !== null && $assoc->isManyToMany()) {
1068 $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
1069 }
1070
1071 if ($assoc !== null && $assoc->isOrdered()) {
1072 $orderBy = $assoc->orderBy();
1073 }
1074
1075 if ($orderBy) {
1076 $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
1077 }
1078
1079 $conditionSql = $criteria instanceof Criteria
1080 ? $this->getSelectConditionCriteriaSQL($criteria)
1081 : $this->getSelectConditionSQL($criteria, $assoc);
1082
1083 $lockSql = match ($lockMode) {
1084 LockMode::PESSIMISTIC_READ => ' ' . $this->getReadLockSQL($this->platform),
1085 LockMode::PESSIMISTIC_WRITE => ' ' . $this->getWriteLockSQL($this->platform),
1086 default => '',
1087 };
1088
1089 $columnList = $this->getSelectColumnsSQL();
1090 $tableAlias = $this->getSQLTableAlias($this->class->name);
1091 $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1092 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1093
1094 if ($filterSql !== '') {
1095 $conditionSql = $conditionSql
1096 ? $conditionSql . ' AND ' . $filterSql
1097 : $filterSql;
1098 }
1099
1100 $select = 'SELECT ' . $columnList;
1101 $from = ' FROM ' . $tableName . ' ' . $tableAlias;
1102 $join = $this->currentPersisterContext->selectJoinSql . $joinSql;
1103 $where = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1104 $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
1105 $query = $select
1106 . $lock
1107 . $join
1108 . $where
1109 . $orderBySql;
1110
1111 return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
1112 }
1113
1114 public function getCountSQL(array|Criteria $criteria = []): string
1115 {
1116 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1117 $tableAlias = $this->getSQLTableAlias($this->class->name);
1118
1119 $conditionSql = $criteria instanceof Criteria
1120 ? $this->getSelectConditionCriteriaSQL($criteria)
1121 : $this->getSelectConditionSQL($criteria);
1122
1123 $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1124
1125 if ($filterSql !== '') {
1126 $conditionSql = $conditionSql
1127 ? $conditionSql . ' AND ' . $filterSql
1128 : $filterSql;
1129 }
1130
1131 return 'SELECT COUNT(*) '
1132 . 'FROM ' . $tableName . ' ' . $tableAlias
1133 . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1134 }
1135
1136 /**
1137 * Gets the ORDER BY SQL snippet for ordered collections.
1138 *
1139 * @psalm-param array<string, string> $orderBy
1140 *
1141 * @throws InvalidOrientation
1142 * @throws InvalidFindByCall
1143 * @throws UnrecognizedField
1144 */
1145 final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string
1146 {
1147 $orderByList = [];
1148
1149 foreach ($orderBy as $fieldName => $orientation) {
1150 $orientation = strtoupper(trim($orientation));
1151
1152 if ($orientation !== 'ASC' && $orientation !== 'DESC') {
1153 throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName);
1154 }
1155
1156 if (isset($this->class->fieldMappings[$fieldName])) {
1157 $tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited)
1158 ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited)
1159 : $baseTableAlias;
1160
1161 $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
1162 $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1163
1164 continue;
1165 }
1166
1167 if (isset($this->class->associationMappings[$fieldName])) {
1168 $association = $this->class->associationMappings[$fieldName];
1169 if (! $association->isOwningSide()) {
1170 throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName);
1171 }
1172
1173 assert($association->isToOneOwningSide());
1174
1175 $tableAlias = isset($association->inherited)
1176 ? $this->getSQLTableAlias($association->inherited)
1177 : $baseTableAlias;
1178
1179 foreach ($association->joinColumns as $joinColumn) {
1180 $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1181 $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1182 }
1183
1184 continue;
1185 }
1186
1187 throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName);
1188 }
1189
1190 return ' ORDER BY ' . implode(', ', $orderByList);
1191 }
1192
1193 /**
1194 * Gets the SQL fragment with the list of columns to select when querying for
1195 * an entity in this persister.
1196 *
1197 * Subclasses should override this method to alter or change the select column
1198 * list SQL fragment. Note that in the implementation of BasicEntityPersister
1199 * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1200 * Subclasses may or may not do the same.
1201 */
1202 protected function getSelectColumnsSQL(): string
1203 {
1204 if ($this->currentPersisterContext->selectColumnListSql !== null) {
1205 return $this->currentPersisterContext->selectColumnListSql;
1206 }
1207
1208 $columnList = [];
1209 $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
1210
1211 // Add regular columns to select list
1212 foreach ($this->class->fieldNames as $field) {
1213 $columnList[] = $this->getSelectColumnSQL($field, $this->class);
1214 }
1215
1216 $this->currentPersisterContext->selectJoinSql = '';
1217 $eagerAliasCounter = 0;
1218
1219 foreach ($this->class->associationMappings as $assocField => $assoc) {
1220 $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
1221
1222 if ($assocColumnSQL) {
1223 $columnList[] = $assocColumnSQL;
1224 }
1225
1226 $isAssocToOneInverseSide = $assoc->isToOne() && ! $assoc->isOwningSide();
1227 $isAssocFromOneEager = $assoc->isToOne() && $assoc->fetch === ClassMetadata::FETCH_EAGER;
1228
1229 if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1230 continue;
1231 }
1232
1233 if ($assoc->isToMany() && $this->currentPersisterContext->handlesLimits) {
1234 continue;
1235 }
1236
1237 $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity);
1238
1239 if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
1240 continue; // now this is why you shouldn't use inheritance
1241 }
1242
1243 $assocAlias = 'e' . ($eagerAliasCounter++);
1244 $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc->targetEntity, $assocAlias, 'r', $assocField);
1245
1246 foreach ($eagerEntity->fieldNames as $field) {
1247 $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1248 }
1249
1250 foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
1251 $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
1252 $eagerAssocField,
1253 $eagerAssoc,
1254 $eagerEntity,
1255 $assocAlias,
1256 );
1257
1258 if ($eagerAssocColumnSQL) {
1259 $columnList[] = $eagerAssocColumnSQL;
1260 }
1261 }
1262
1263 $association = $assoc;
1264 $joinCondition = [];
1265
1266 if ($assoc->isIndexed()) {
1267 assert($assoc->isToMany());
1268 $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc->indexBy());
1269 }
1270
1271 if (! $assoc->isOwningSide()) {
1272 $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity);
1273 $association = $eagerEntity->getAssociationMapping($assoc->mappedBy);
1274 }
1275
1276 assert($association->isToOneOwningSide());
1277
1278 $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
1279 $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
1280
1281 if ($assoc->isOwningSide()) {
1282 $tableAlias = $this->getSQLTableAlias($association->targetEntity, $assocAlias);
1283 $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association->joinColumns);
1284
1285 foreach ($association->joinColumns as $joinColumn) {
1286 $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1287 $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1288 $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity)
1289 . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
1290 }
1291
1292 // Add filter SQL
1293 $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias);
1294 if ($filterSql) {
1295 $joinCondition[] = $filterSql;
1296 }
1297 } else {
1298 $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
1299
1300 foreach ($association->joinColumns as $joinColumn) {
1301 $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1302 $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1303
1304 $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity, $assocAlias) . '.' . $sourceCol . ' = '
1305 . $this->getSQLTableAlias($association->targetEntity) . '.' . $targetCol;
1306 }
1307 }
1308
1309 $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1310 $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1311 }
1312
1313 $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1314
1315 return $this->currentPersisterContext->selectColumnListSql;
1316 }
1317
1318 /** Gets the SQL join fragment used when selecting entities from an association. */
1319 protected function getSelectColumnAssociationSQL(
1320 string $field,
1321 AssociationMapping $assoc,
1322 ClassMetadata $class,
1323 string $alias = 'r',
1324 ): string {
1325 if (! $assoc->isToOneOwningSide()) {
1326 return '';
1327 }
1328
1329 $columnList = [];
1330 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
1331 $isIdentifier = isset($assoc->id) && $assoc->id === true;
1332 $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias));
1333
1334 foreach ($assoc->joinColumns as $joinColumn) {
1335 $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1336 $resultColumnName = $this->getSQLColumnAlias($joinColumn->name);
1337 $type = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
1338
1339 $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn->name, $isIdentifier, $type);
1340
1341 $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
1342 }
1343
1344 return implode(', ', $columnList);
1345 }
1346
1347 /**
1348 * Gets the SQL join fragment used when selecting entities from a
1349 * many-to-many association.
1350 */
1351 protected function getSelectManyToManyJoinSQL(AssociationMapping&ManyToManyAssociationMapping $manyToMany): string
1352 {
1353 $conditions = [];
1354 $association = $manyToMany;
1355 $sourceTableAlias = $this->getSQLTableAlias($this->class->name);
1356
1357 $association = $this->em->getMetadataFactory()->getOwningSide($manyToMany);
1358 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
1359 $joinColumns = $manyToMany->isOwningSide()
1360 ? $association->joinTable->inverseJoinColumns
1361 : $association->joinTable->joinColumns;
1362
1363 foreach ($joinColumns as $joinColumn) {
1364 $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1365 $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1366 $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
1367 }
1368
1369 return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1370 }
1371
1372 public function getInsertSQL(): string
1373 {
1374 if ($this->insertSql !== null) {
1375 return $this->insertSql;
1376 }
1377
1378 $columns = $this->getInsertColumnList();
1379 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1380
1381 if (empty($columns)) {
1382 $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
1383 $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1384
1385 return $this->insertSql;
1386 }
1387
1388 $values = [];
1389 $columns = array_unique($columns);
1390
1391 foreach ($columns as $column) {
1392 $placeholder = '?';
1393
1394 if (
1395 isset($this->class->fieldNames[$column])
1396 && isset($this->columnTypes[$this->class->fieldNames[$column]])
1397 && isset($this->class->fieldMappings[$this->class->fieldNames[$column]])
1398 ) {
1399 $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
1400 $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
1401 }
1402
1403 $values[] = $placeholder;
1404 }
1405
1406 $columns = implode(', ', $columns);
1407 $values = implode(', ', $values);
1408
1409 $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
1410
1411 return $this->insertSql;
1412 }
1413
1414 /**
1415 * Gets the list of columns to put in the INSERT SQL statement.
1416 *
1417 * Subclasses should override this method to alter or change the list of
1418 * columns placed in the INSERT statements used by the persister.
1419 *
1420 * @psalm-return list<string>
1421 */
1422 protected function getInsertColumnList(): array
1423 {
1424 $columns = [];
1425
1426 foreach ($this->class->reflFields as $name => $field) {
1427 if ($this->class->isVersioned && $this->class->versionField === $name) {
1428 continue;
1429 }
1430
1431 if (isset($this->class->embeddedClasses[$name])) {
1432 continue;
1433 }
1434
1435 if (isset($this->class->associationMappings[$name])) {
1436 $assoc = $this->class->associationMappings[$name];
1437
1438 if ($assoc->isToOneOwningSide()) {
1439 foreach ($assoc->joinColumns as $joinColumn) {
1440 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1441 }
1442 }
1443
1444 continue;
1445 }
1446
1447 if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {
1448 if (isset($this->class->fieldMappings[$name]->notInsertable)) {
1449 continue;
1450 }
1451
1452 $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
1453 $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type;
1454 }
1455 }
1456
1457 return $columns;
1458 }
1459
1460 /**
1461 * Gets the SQL snippet of a qualified column name for the given field name.
1462 *
1463 * @param ClassMetadata $class The class that declares this field. The table this class is
1464 * mapped to must own the column for the given field.
1465 */
1466 protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string
1467 {
1468 $root = $alias === 'r' ? '' : $alias;
1469 $tableAlias = $this->getSQLTableAlias($class->name, $root);
1470 $fieldMapping = $class->fieldMappings[$field];
1471 $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
1472 $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
1473
1474 $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
1475 if (! empty($fieldMapping->enumType)) {
1476 $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping->enumType);
1477 }
1478
1479 $type = Type::getType($fieldMapping->type);
1480 $sql = $type->convertToPHPValueSQL($sql, $this->platform);
1481
1482 return $sql . ' AS ' . $columnAlias;
1483 }
1484
1485 /**
1486 * Gets the SQL table alias for the given class name.
1487 *
1488 * @todo Reconsider. Binding table aliases to class names is not such a good idea.
1489 */
1490 protected function getSQLTableAlias(string $className, string $assocName = ''): string
1491 {
1492 if ($assocName) {
1493 $className .= '#' . $assocName;
1494 }
1495
1496 if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
1497 return $this->currentPersisterContext->sqlTableAliases[$className];
1498 }
1499
1500 $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1501
1502 $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
1503
1504 return $tableAlias;
1505 }
1506
1507 /**
1508 * {@inheritDoc}
1509 */
1510 public function lock(array $criteria, LockMode|int $lockMode): void
1511 {
1512 $conditionSql = $this->getSelectConditionSQL($criteria);
1513
1514 $lockSql = match ($lockMode) {
1515 LockMode::PESSIMISTIC_READ => $this->getReadLockSQL($this->platform),
1516 LockMode::PESSIMISTIC_WRITE => $this->getWriteLockSQL($this->platform),
1517 default => '',
1518 };
1519
1520 $lock = $this->getLockTablesSql($lockMode);
1521 $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1522 $sql = 'SELECT 1 '
1523 . $lock
1524 . $where
1525 . $lockSql;
1526
1527 [$params, $types] = $this->expandParameters($criteria);
1528
1529 $this->conn->executeQuery($sql, $params, $types);
1530 }
1531
1532 /**
1533 * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1534 *
1535 * @psalm-param LockMode::* $lockMode
1536 */
1537 protected function getLockTablesSql(LockMode|int $lockMode): string
1538 {
1539 return $this->platform->appendLockHint(
1540 'FROM '
1541 . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
1542 . $this->getSQLTableAlias($this->class->name),
1543 $lockMode,
1544 );
1545 }
1546
1547 /**
1548 * Gets the Select Where Condition from a Criteria object.
1549 */
1550 protected function getSelectConditionCriteriaSQL(Criteria $criteria): string
1551 {
1552 $expression = $criteria->getWhereExpression();
1553
1554 if ($expression === null) {
1555 return '';
1556 }
1557
1558 $visitor = new SqlExpressionVisitor($this, $this->class);
1559
1560 return $visitor->dispatch($expression);
1561 }
1562
1563 public function getSelectConditionStatementSQL(
1564 string $field,
1565 mixed $value,
1566 AssociationMapping|null $assoc = null,
1567 string|null $comparison = null,
1568 ): string {
1569 $selectedColumns = [];
1570 $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1571
1572 if (count($columns) > 1 && $comparison === Comparison::IN) {
1573 /*
1574 * @todo try to support multi-column IN expressions.
1575 * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
1576 */
1577 throw CantUseInOperatorOnCompositeKeys::create();
1578 }
1579
1580 foreach ($columns as $column) {
1581 $placeholder = '?';
1582
1583 if (isset($this->class->fieldMappings[$field])) {
1584 $type = Type::getType($this->class->fieldMappings[$field]->type);
1585 $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
1586 }
1587
1588 if ($comparison !== null) {
1589 // special case null value handling
1590 if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
1591 $selectedColumns[] = $column . ' IS NULL';
1592
1593 continue;
1594 }
1595
1596 if ($comparison === Comparison::NEQ && $value === null) {
1597 $selectedColumns[] = $column . ' IS NOT NULL';
1598
1599 continue;
1600 }
1601
1602 $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1603
1604 continue;
1605 }
1606
1607 if (is_array($value)) {
1608 $in = sprintf('%s IN (%s)', $column, $placeholder);
1609
1610 if (array_search(null, $value, true) !== false) {
1611 $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1612
1613 continue;
1614 }
1615
1616 $selectedColumns[] = $in;
1617
1618 continue;
1619 }
1620
1621 if ($value === null) {
1622 $selectedColumns[] = sprintf('%s IS NULL', $column);
1623
1624 continue;
1625 }
1626
1627 $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1628 }
1629
1630 return implode(' AND ', $selectedColumns);
1631 }
1632
1633 /**
1634 * Builds the left-hand-side of a where condition statement.
1635 *
1636 * @return string[]
1637 * @psalm-return list<string>
1638 *
1639 * @throws InvalidFindByCall
1640 * @throws UnrecognizedField
1641 */
1642 private function getSelectConditionStatementColumnSQL(
1643 string $field,
1644 AssociationMapping|null $assoc = null,
1645 ): array {
1646 if (isset($this->class->fieldMappings[$field])) {
1647 $className = $this->class->fieldMappings[$field]->inherited ?? $this->class->name;
1648
1649 return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
1650 }
1651
1652 if (isset($this->class->associationMappings[$field])) {
1653 $association = $this->class->associationMappings[$field];
1654 // Many-To-Many requires join table check for joinColumn
1655 $columns = [];
1656 $class = $this->class;
1657
1658 if ($association->isManyToMany()) {
1659 assert($assoc !== null);
1660 if (! $association->isOwningSide()) {
1661 $association = $assoc;
1662 }
1663
1664 assert($association->isManyToManyOwningSide());
1665
1666 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1667 $joinColumns = $assoc->isOwningSide()
1668 ? $association->joinTable->joinColumns
1669 : $association->joinTable->inverseJoinColumns;
1670
1671 foreach ($joinColumns as $joinColumn) {
1672 $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1673 }
1674 } else {
1675 if (! $association->isOwningSide()) {
1676 throw InvalidFindByCall::fromInverseSideUsage(
1677 $this->class->name,
1678 $field,
1679 );
1680 }
1681
1682 assert($association->isToOneOwningSide());
1683
1684 $className = $association->inherited ?? $this->class->name;
1685
1686 foreach ($association->joinColumns as $joinColumn) {
1687 $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1688 }
1689 }
1690
1691 return $columns;
1692 }
1693
1694 if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) {
1695 // very careless developers could potentially open up this normally hidden api for userland attacks,
1696 // therefore checking for spaces and function calls which are not allowed.
1697
1698 // found a join column condition, not really a "field"
1699 return [$field];
1700 }
1701
1702 throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field);
1703 }
1704
1705 /**
1706 * Gets the conditional SQL fragment used in the WHERE clause when selecting
1707 * entities in this persister.
1708 *
1709 * Subclasses are supposed to override this method if they intend to change
1710 * or alter the criteria by which entities are selected.
1711 *
1712 * @psalm-param array<string, mixed> $criteria
1713 */
1714 protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string
1715 {
1716 $conditions = [];
1717
1718 foreach ($criteria as $field => $value) {
1719 $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
1720 }
1721
1722 return implode(' AND ', $conditions);
1723 }
1724
1725 /**
1726 * {@inheritDoc}
1727 */
1728 public function getOneToManyCollection(
1729 AssociationMapping $assoc,
1730 object $sourceEntity,
1731 int|null $offset = null,
1732 int|null $limit = null,
1733 ): array {
1734 assert($assoc instanceof OneToManyAssociationMapping);
1735 $this->switchPersisterContext($offset, $limit);
1736
1737 $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1738
1739 return $this->loadArrayFromResult($assoc, $stmt);
1740 }
1741
1742 public function loadOneToManyCollection(
1743 AssociationMapping $assoc,
1744 object $sourceEntity,
1745 PersistentCollection $collection,
1746 ): mixed {
1747 assert($assoc instanceof OneToManyAssociationMapping);
1748 $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1749
1750 return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
1751 }
1752
1753 /** Builds criteria and execute SQL statement to fetch the one to many entities from. */
1754 private function getOneToManyStatement(
1755 OneToManyAssociationMapping $assoc,
1756 object $sourceEntity,
1757 int|null $offset = null,
1758 int|null $limit = null,
1759 ): Result {
1760 $this->switchPersisterContext($offset, $limit);
1761
1762 $criteria = [];
1763 $parameters = [];
1764 $owningAssoc = $this->class->associationMappings[$assoc->mappedBy];
1765 $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity);
1766 $tableAlias = $this->getSQLTableAlias($owningAssoc->inherited ?? $this->class->name);
1767 assert($owningAssoc->isManyToOne());
1768
1769 foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
1770 if ($sourceClass->containsForeignIdentifier) {
1771 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1772 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1773
1774 if (isset($sourceClass->associationMappings[$field])) {
1775 $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1776 $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]];
1777 }
1778
1779 $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
1780 $parameters[] = [
1781 'value' => $value,
1782 'field' => $field,
1783 'class' => $sourceClass,
1784 ];
1785
1786 continue;
1787 }
1788
1789 $field = $sourceClass->fieldNames[$sourceKeyColumn];
1790 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1791
1792 $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
1793 $parameters[] = [
1794 'value' => $value,
1795 'field' => $field,
1796 'class' => $sourceClass,
1797 ];
1798 }
1799
1800 $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1801 [$params, $types] = $this->expandToManyParameters($parameters);
1802
1803 return $this->conn->executeQuery($sql, $params, $types);
1804 }
1805
1806 /**
1807 * {@inheritDoc}
1808 */
1809 public function expandParameters(array $criteria): array
1810 {
1811 $params = [];
1812 $types = [];
1813
1814 foreach ($criteria as $field => $value) {
1815 if ($value === null) {
1816 continue; // skip null values.
1817 }
1818
1819 $types = [...$types, ...$this->getTypes($field, $value, $this->class)];
1820 $params = array_merge($params, $this->getValues($value));
1821 }
1822
1823 return [$params, $types];
1824 }
1825
1826 /**
1827 * Expands the parameters from the given criteria and use the correct binding types if found,
1828 * specialized for OneToMany or ManyToMany associations.
1829 *
1830 * @param mixed[][] $criteria an array of arrays containing following:
1831 * - field to which each criterion will be bound
1832 * - value to be bound
1833 * - class to which the field belongs to
1834 *
1835 * @return mixed[][]
1836 * @psalm-return array{0: array, 1: list<ParameterType::*|ArrayParameterType::*|string>}
1837 */
1838 private function expandToManyParameters(array $criteria): array
1839 {
1840 $params = [];
1841 $types = [];
1842
1843 foreach ($criteria as $criterion) {
1844 if ($criterion['value'] === null) {
1845 continue; // skip null values.
1846 }
1847
1848 $types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])];
1849 $params = array_merge($params, $this->getValues($criterion['value']));
1850 }
1851
1852 return [$params, $types];
1853 }
1854
1855 /**
1856 * Infers field types to be used by parameter type casting.
1857 *
1858 * @return list<ParameterType|ArrayParameterType|int|string>
1859 * @psalm-return list<ParameterType::*|ArrayParameterType::*|string>
1860 *
1861 * @throws QueryException
1862 */
1863 private function getTypes(string $field, mixed $value, ClassMetadata $class): array
1864 {
1865 $types = [];
1866
1867 switch (true) {
1868 case isset($class->fieldMappings[$field]):
1869 $types = array_merge($types, [$class->fieldMappings[$field]->type]);
1870 break;
1871
1872 case isset($class->associationMappings[$field]):
1873 $assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]);
1874 $class = $this->em->getClassMetadata($assoc->targetEntity);
1875
1876 if ($assoc->isManyToManyOwningSide()) {
1877 $columns = $assoc->relationToTargetKeyColumns;
1878 } else {
1879 assert($assoc->isToOneOwningSide());
1880 $columns = $assoc->sourceToTargetKeyColumns;
1881 }
1882
1883 foreach ($columns as $column) {
1884 $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
1885 }
1886
1887 break;
1888
1889 default:
1890 $types[] = ParameterType::STRING;
1891 break;
1892 }
1893
1894 if (is_array($value)) {
1895 return array_map($this->getArrayBindingType(...), $types);
1896 }
1897
1898 return $types;
1899 }
1900
1901 /** @psalm-return ArrayParameterType::* */
1902 private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int
1903 {
1904 if (! $type instanceof ParameterType) {
1905 $type = Type::getType((string) $type)->getBindingType();
1906 }
1907
1908 return match ($type) {
1909 ParameterType::STRING => ArrayParameterType::STRING,
1910 ParameterType::INTEGER => ArrayParameterType::INTEGER,
1911 ParameterType::ASCII => ArrayParameterType::ASCII,
1912 };
1913 }
1914
1915 /**
1916 * Retrieves the parameters that identifies a value.
1917 *
1918 * @return mixed[]
1919 */
1920 private function getValues(mixed $value): array
1921 {
1922 if (is_array($value)) {
1923 $newValue = [];
1924
1925 foreach ($value as $itemValue) {
1926 $newValue = array_merge($newValue, $this->getValues($itemValue));
1927 }
1928
1929 return [$newValue];
1930 }
1931
1932 return $this->getIndividualValue($value);
1933 }
1934
1935 /**
1936 * Retrieves an individual parameter value.
1937 *
1938 * @psalm-return list<mixed>
1939 */
1940 private function getIndividualValue(mixed $value): array
1941 {
1942 if (! is_object($value)) {
1943 return [$value];
1944 }
1945
1946 if ($value instanceof BackedEnum) {
1947 return [$value->value];
1948 }
1949
1950 $valueClass = DefaultProxyClassNameResolver::getClass($value);
1951
1952 if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
1953 return [$value];
1954 }
1955
1956 $class = $this->em->getClassMetadata($valueClass);
1957
1958 if ($class->isIdentifierComposite) {
1959 $newValue = [];
1960
1961 foreach ($class->getIdentifierValues($value) as $innerValue) {
1962 $newValue = array_merge($newValue, $this->getValues($innerValue));
1963 }
1964
1965 return $newValue;
1966 }
1967
1968 return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
1969 }
1970
1971 public function exists(object $entity, Criteria|null $extraConditions = null): bool
1972 {
1973 $criteria = $this->class->getIdentifierValues($entity);
1974
1975 if (! $criteria) {
1976 return false;
1977 }
1978
1979 $alias = $this->getSQLTableAlias($this->class->name);
1980
1981 $sql = 'SELECT 1 '
1982 . $this->getLockTablesSql(LockMode::NONE)
1983 . ' WHERE ' . $this->getSelectConditionSQL($criteria);
1984
1985 [$params, $types] = $this->expandParameters($criteria);
1986
1987 if ($extraConditions !== null) {
1988 $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
1989 [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);
1990
1991 $params = [...$params, ...$criteriaParams];
1992 $types = [...$types, ...$criteriaTypes];
1993 }
1994
1995 $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
1996 if ($filterSql) {
1997 $sql .= ' AND ' . $filterSql;
1998 }
1999
2000 return (bool) $this->conn->fetchOne($sql, $params, $types);
2001 }
2002
2003 /**
2004 * Generates the appropriate join SQL for the given join column.
2005 *
2006 * @param list<JoinColumnMapping> $joinColumns The join columns definition of an association.
2007 *
2008 * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2009 */
2010 protected function getJoinSQLForJoinColumns(array $joinColumns): string
2011 {
2012 // if one of the join columns is nullable, return left join
2013 foreach ($joinColumns as $joinColumn) {
2014 if (! isset($joinColumn->nullable) || $joinColumn->nullable) {
2015 return 'LEFT JOIN';
2016 }
2017 }
2018
2019 return 'INNER JOIN';
2020 }
2021
2022 public function getSQLColumnAlias(string $columnName): string
2023 {
2024 return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
2025 }
2026
2027 /**
2028 * Generates the filter SQL for a given entity and table alias.
2029 *
2030 * @param ClassMetadata $targetEntity Metadata of the target entity.
2031 * @param string $targetTableAlias The table alias of the joined/selected table.
2032 *
2033 * @return string The SQL query part to add to a query.
2034 */
2035 protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string
2036 {
2037 $filterClauses = [];
2038
2039 foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2040 $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
2041 if ($filterExpr !== '') {
2042 $filterClauses[] = '(' . $filterExpr . ')';
2043 }
2044 }
2045
2046 $sql = implode(' AND ', $filterClauses);
2047
2048 return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
2049 }
2050
2051 /**
2052 * Switches persister context according to current query offset/limits
2053 *
2054 * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2055 */
2056 protected function switchPersisterContext(int|null $offset, int|null $limit): void
2057 {
2058 if ($offset === null && $limit === null) {
2059 $this->currentPersisterContext = $this->noLimitsContext;
2060
2061 return;
2062 }
2063
2064 $this->currentPersisterContext = $this->limitsHandlingContext;
2065 }
2066
2067 /**
2068 * @return string[]
2069 * @psalm-return list<string>
2070 */
2071 protected function getClassIdentifiersTypes(ClassMetadata $class): array
2072 {
2073 $entityManager = $this->em;
2074
2075 return array_map(
2076 static function ($fieldName) use ($class, $entityManager): string {
2077 $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager);
2078 assert(isset($types[0]));
2079
2080 return $types[0];
2081 },
2082 $class->identifier,
2083 );
2084 }
2085}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php
new file mode 100644
index 0000000..03d053b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php
@@ -0,0 +1,60 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\ORM\Query\ResultSetMapping;
8use Doctrine\Persistence\Mapping\ClassMetadata;
9
10/**
11 * A swappable persister context to use as a container for the current
12 * generated query/resultSetMapping/type binding information.
13 *
14 * This class is a utility class to be used only by the persister API
15 *
16 * This object is highly mutable due to performance reasons. Same reasoning
17 * behind its properties being public.
18 */
19class CachedPersisterContext
20{
21 /**
22 * The SELECT column list SQL fragment used for querying entities by this persister.
23 * This SQL fragment is only generated once per request, if at all.
24 */
25 public string|null $selectColumnListSql = null;
26
27 /**
28 * The JOIN SQL fragment used to eagerly load all many-to-one and one-to-one
29 * associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
30 */
31 public string|null $selectJoinSql = null;
32
33 /**
34 * Counter for creating unique SQL table and column aliases.
35 */
36 public int $sqlAliasCounter = 0;
37
38 /**
39 * Map from class names (FQCN) to the corresponding generated SQL table aliases.
40 *
41 * @var array<class-string, string>
42 */
43 public array $sqlTableAliases = [];
44
45 public function __construct(
46 /**
47 * Metadata object that describes the mapping of the mapped entity class.
48 */
49 public ClassMetadata $class,
50 /**
51 * ResultSetMapping that is used for all queries. Is generated lazily once per request.
52 */
53 public ResultSetMapping $rsm,
54 /**
55 * Whether this persistent context is considering limit operations applied to the selection queries
56 */
57 public bool $handlesLimits,
58 ) {
59 }
60}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php
new file mode 100644
index 0000000..6b278a7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php
@@ -0,0 +1,298 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\DBAL\ArrayParameterType;
9use Doctrine\DBAL\LockMode;
10use Doctrine\DBAL\ParameterType;
11use Doctrine\ORM\Mapping\AssociationMapping;
12use Doctrine\ORM\Mapping\ClassMetadata;
13use Doctrine\ORM\Mapping\MappingException;
14use Doctrine\ORM\PersistentCollection;
15use Doctrine\ORM\Query\ResultSetMapping;
16
17/**
18 * Entity persister interface
19 * Define the behavior that should be implemented by all entity persisters.
20 */
21interface EntityPersister
22{
23 public function getClassMetadata(): ClassMetadata;
24
25 /**
26 * Gets the ResultSetMapping used for hydration.
27 */
28 public function getResultSetMapping(): ResultSetMapping;
29
30 /**
31 * Get all queued inserts.
32 *
33 * @return object[]
34 */
35 public function getInserts(): array;
36
37 /**
38 * Gets the INSERT SQL used by the persister to persist a new entity.
39 *
40 * @TODO It should not be here.
41 * But its necessary since JoinedSubclassPersister#executeInserts invoke the root persister.
42 */
43 public function getInsertSQL(): string;
44
45 /**
46 * Gets the SELECT SQL to select one or more entities by a set of field criteria.
47 *
48 * @param mixed[]|Criteria $criteria
49 * @param mixed[]|null $orderBy
50 * @psalm-param AssociationMapping|null $assoc
51 * @psalm-param LockMode::*|null $lockMode
52 */
53 public function getSelectSQL(
54 array|Criteria $criteria,
55 AssociationMapping|null $assoc = null,
56 LockMode|int|null $lockMode = null,
57 int|null $limit = null,
58 int|null $offset = null,
59 array|null $orderBy = null,
60 ): string;
61
62 /**
63 * Get the COUNT SQL to count entities (optionally based on a criteria)
64 *
65 * @param mixed[]|Criteria $criteria
66 */
67 public function getCountSQL(array|Criteria $criteria = []): string;
68
69 /**
70 * Expands the parameters from the given criteria and use the correct binding types if found.
71 *
72 * @param string[] $criteria
73 *
74 * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>}
75 */
76 public function expandParameters(array $criteria): array;
77
78 /**
79 * Expands Criteria Parameters by walking the expressions and grabbing all parameters and types from it.
80 *
81 * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>}
82 */
83 public function expandCriteriaParameters(Criteria $criteria): array;
84
85 /** Gets the SQL WHERE condition for matching a field with a given value. */
86 public function getSelectConditionStatementSQL(
87 string $field,
88 mixed $value,
89 AssociationMapping|null $assoc = null,
90 string|null $comparison = null,
91 ): string;
92
93 /**
94 * Adds an entity to the queued insertions.
95 * The entity remains queued until {@link executeInserts} is invoked.
96 */
97 public function addInsert(object $entity): void;
98
99 /**
100 * Executes all queued entity insertions.
101 *
102 * If no inserts are queued, invoking this method is a NOOP.
103 */
104 public function executeInserts(): void;
105
106 /**
107 * Updates a managed entity. The entity is updated according to its current changeset
108 * in the running UnitOfWork. If there is no changeset, nothing is updated.
109 */
110 public function update(object $entity): void;
111
112 /**
113 * Deletes a managed entity.
114 *
115 * The entity to delete must be managed and have a persistent identifier.
116 * The deletion happens instantaneously.
117 *
118 * Subclasses may override this method to customize the semantics of entity deletion.
119 *
120 * @return bool TRUE if the entity got deleted in the database, FALSE otherwise.
121 */
122 public function delete(object $entity): bool;
123
124 /**
125 * Count entities (optionally filtered by a criteria)
126 *
127 * @param mixed[]|Criteria $criteria
128 */
129 public function count(array|Criteria $criteria = []): int;
130
131 /**
132 * Gets the name of the table that owns the column the given field is mapped to.
133 *
134 * The default implementation in BasicEntityPersister always returns the name
135 * of the table the entity type of this persister is mapped to, since an entity
136 * is always persisted to a single table with a BasicEntityPersister.
137 */
138 public function getOwningTable(string $fieldName): string;
139
140 /**
141 * Loads an entity by a list of field criteria.
142 *
143 * @param mixed[] $criteria The criteria by which to load the entity.
144 * @param object|null $entity The entity to load the data into. If not specified,
145 * a new entity is created.
146 * @param AssociationMapping|null $assoc The association that connects the entity
147 * to load to another entity, if any.
148 * @param mixed[] $hints Hints for entity creation.
149 * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
150 * or NULL if no specific lock mode should be used
151 * for loading the entity.
152 * @param int|null $limit Limit number of results.
153 * @param string[]|null $orderBy Criteria to order by.
154 * @psalm-param array<string, mixed> $criteria
155 * @psalm-param array<string, mixed> $hints
156 * @psalm-param LockMode::*|null $lockMode
157 * @psalm-param array<string, string>|null $orderBy
158 *
159 * @return object|null The loaded and managed entity instance or NULL if the entity can not be found.
160 *
161 * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
162 */
163 public function load(
164 array $criteria,
165 object|null $entity = null,
166 AssociationMapping|null $assoc = null,
167 array $hints = [],
168 LockMode|int|null $lockMode = null,
169 int|null $limit = null,
170 array|null $orderBy = null,
171 ): object|null;
172
173 /**
174 * Loads an entity by identifier.
175 *
176 * @param object|null $entity The entity to load the data into. If not specified, a new entity is created.
177 * @psalm-param array<string, mixed> $identifier The entity identifier.
178 *
179 * @return object|null The loaded and managed entity instance or NULL if the entity can not be found.
180 *
181 * @todo Check parameters
182 */
183 public function loadById(array $identifier, object|null $entity = null): object|null;
184
185 /**
186 * Loads an entity of this persister's mapped class as part of a single-valued
187 * association from another entity.
188 *
189 * @param AssociationMapping $assoc The association to load.
190 * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side").
191 * @psalm-param array<string, mixed> $identifier The identifier of the entity to load. Must be provided if
192 * the association to load represents the owning side, otherwise
193 * the identifier is derived from the $sourceEntity.
194 *
195 * @return object|null The loaded and managed entity instance or NULL if the entity can not be found.
196 *
197 * @throws MappingException
198 */
199 public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null;
200
201 /**
202 * Refreshes a managed entity.
203 *
204 * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
205 * or NULL if no specific lock mode should be used
206 * for refreshing the managed entity.
207 * @psalm-param array<string, mixed> $id The identifier of the entity as an
208 * associative array from column or
209 * field names to values.
210 * @psalm-param LockMode::*|null $lockMode
211 */
212 public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void;
213
214 /**
215 * Loads Entities matching the given Criteria object.
216 *
217 * @return mixed[]
218 */
219 public function loadCriteria(Criteria $criteria): array;
220
221 /**
222 * Loads a list of entities by a list of field criteria.
223 *
224 * @psalm-param array<string, string>|null $orderBy
225 * @psalm-param array<string, mixed> $criteria
226 *
227 * @return mixed[]
228 */
229 public function loadAll(
230 array $criteria = [],
231 array|null $orderBy = null,
232 int|null $limit = null,
233 int|null $offset = null,
234 ): array;
235
236 /**
237 * Gets (sliced or full) elements of the given collection.
238 *
239 * @return mixed[]
240 */
241 public function getManyToManyCollection(
242 AssociationMapping $assoc,
243 object $sourceEntity,
244 int|null $offset = null,
245 int|null $limit = null,
246 ): array;
247
248 /**
249 * Loads a collection of entities of a many-to-many association.
250 *
251 * @param AssociationMapping $assoc The association mapping of the association being loaded.
252 * @param object $sourceEntity The entity that owns the collection.
253 * @param PersistentCollection $collection The collection to fill.
254 *
255 * @return mixed[]
256 */
257 public function loadManyToManyCollection(
258 AssociationMapping $assoc,
259 object $sourceEntity,
260 PersistentCollection $collection,
261 ): array;
262
263 /**
264 * Loads a collection of entities in a one-to-many association.
265 *
266 * @param PersistentCollection $collection The collection to load/fill.
267 */
268 public function loadOneToManyCollection(
269 AssociationMapping $assoc,
270 object $sourceEntity,
271 PersistentCollection $collection,
272 ): mixed;
273
274 /**
275 * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode.
276 *
277 * @psalm-param array<string, mixed> $criteria
278 * @psalm-param LockMode::* $lockMode
279 */
280 public function lock(array $criteria, LockMode|int $lockMode): void;
281
282 /**
283 * Returns an array with (sliced or full list) of elements in the specified collection.
284 *
285 * @return mixed[]
286 */
287 public function getOneToManyCollection(
288 AssociationMapping $assoc,
289 object $sourceEntity,
290 int|null $offset = null,
291 int|null $limit = null,
292 ): array;
293
294 /**
295 * Checks whether the given managed entity exists in the database.
296 */
297 public function exists(object $entity, Criteria|null $extraConditions = null): bool;
298}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
new file mode 100644
index 0000000..76719a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
@@ -0,0 +1,601 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\DBAL\LockMode;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\DBAL\Types\Types;
11use Doctrine\ORM\Internal\SQLResultCasing;
12use Doctrine\ORM\Mapping\AssociationMapping;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Utility\LockSqlHelper;
15use Doctrine\ORM\Utility\PersisterHelper;
16use LengthException;
17
18use function array_combine;
19use function array_keys;
20use function array_values;
21use function implode;
22
23/**
24 * The joined subclass persister maps a single entity instance to several tables in the
25 * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
26 *
27 * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html
28 */
29class JoinedSubclassPersister extends AbstractEntityInheritancePersister
30{
31 use LockSqlHelper;
32 use SQLResultCasing;
33
34 /**
35 * Map that maps column names to the table names that own them.
36 * This is mainly a temporary cache, used during a single request.
37 *
38 * @psalm-var array<string, string>
39 */
40 private array $owningTableMap = [];
41
42 /**
43 * Map of table to quoted table names.
44 *
45 * @psalm-var array<string, string>
46 */
47 private array $quotedTableMap = [];
48
49 protected function getDiscriminatorColumnTableName(): string
50 {
51 $class = $this->class->name !== $this->class->rootEntityName
52 ? $this->em->getClassMetadata($this->class->rootEntityName)
53 : $this->class;
54
55 return $class->getTableName();
56 }
57
58 /**
59 * This function finds the ClassMetadata instance in an inheritance hierarchy
60 * that is responsible for enabling versioning.
61 */
62 private function getVersionedClassMetadata(): ClassMetadata
63 {
64 if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) {
65 $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited;
66
67 return $this->em->getClassMetadata($definingClassName);
68 }
69
70 return $this->class;
71 }
72
73 /**
74 * Gets the name of the table that owns the column the given field is mapped to.
75 */
76 public function getOwningTable(string $fieldName): string
77 {
78 if (isset($this->owningTableMap[$fieldName])) {
79 return $this->owningTableMap[$fieldName];
80 }
81
82 $cm = match (true) {
83 isset($this->class->associationMappings[$fieldName]->inherited)
84 => $this->em->getClassMetadata($this->class->associationMappings[$fieldName]->inherited),
85 isset($this->class->fieldMappings[$fieldName]->inherited)
86 => $this->em->getClassMetadata($this->class->fieldMappings[$fieldName]->inherited),
87 default => $this->class,
88 };
89
90 $tableName = $cm->getTableName();
91 $quotedTableName = $this->quoteStrategy->getTableName($cm, $this->platform);
92
93 $this->owningTableMap[$fieldName] = $tableName;
94 $this->quotedTableMap[$tableName] = $quotedTableName;
95
96 return $tableName;
97 }
98
99 public function executeInserts(): void
100 {
101 if (! $this->queuedInserts) {
102 return;
103 }
104
105 $uow = $this->em->getUnitOfWork();
106 $idGenerator = $this->class->idGenerator;
107 $isPostInsertId = $idGenerator->isPostInsertGenerator();
108 $rootClass = $this->class->name !== $this->class->rootEntityName
109 ? $this->em->getClassMetadata($this->class->rootEntityName)
110 : $this->class;
111
112 // Prepare statement for the root table
113 $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->name);
114 $rootTableName = $rootClass->getTableName();
115 $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL());
116
117 // Prepare statements for sub tables.
118 $subTableStmts = [];
119
120 if ($rootClass !== $this->class) {
121 $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL());
122 }
123
124 foreach ($this->class->parentClasses as $parentClassName) {
125 $parentClass = $this->em->getClassMetadata($parentClassName);
126 $parentTableName = $parentClass->getTableName();
127
128 if ($parentClass !== $rootClass) {
129 $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClassName);
130 $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL());
131 }
132 }
133
134 // Execute all inserts. For each entity:
135 // 1) Insert on root table
136 // 2) Insert on sub tables
137 foreach ($this->queuedInserts as $entity) {
138 $insertData = $this->prepareInsertData($entity);
139
140 // Execute insert on root table
141 $paramIndex = 1;
142
143 foreach ($insertData[$rootTableName] as $columnName => $value) {
144 $rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]);
145 }
146
147 $rootTableStmt->executeStatement();
148
149 if ($isPostInsertId) {
150 $generatedId = $idGenerator->generateId($this->em, $entity);
151 $id = [$this->class->identifier[0] => $generatedId];
152
153 $uow->assignPostInsertId($entity, $generatedId);
154 } else {
155 $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
156 }
157
158 // Execute inserts on subtables.
159 // The order doesn't matter because all child tables link to the root table via FK.
160 foreach ($subTableStmts as $tableName => $stmt) {
161 $paramIndex = 1;
162 $data = $insertData[$tableName] ?? [];
163
164 foreach ($id as $idName => $idVal) {
165 $type = $this->columnTypes[$idName] ?? Types::STRING;
166
167 $stmt->bindValue($paramIndex++, $idVal, $type);
168 }
169
170 foreach ($data as $columnName => $value) {
171 if (! isset($id[$columnName])) {
172 $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]);
173 }
174 }
175
176 $stmt->executeStatement();
177 }
178
179 if ($this->class->requiresFetchAfterChange) {
180 $this->assignDefaultVersionAndUpsertableValues($entity, $id);
181 }
182 }
183
184 $this->queuedInserts = [];
185 }
186
187 public function update(object $entity): void
188 {
189 $updateData = $this->prepareUpdateData($entity);
190
191 if (! $updateData) {
192 return;
193 }
194
195 $isVersioned = $this->class->isVersioned;
196
197 $versionedClass = $this->getVersionedClassMetadata();
198 $versionedTable = $versionedClass->getTableName();
199
200 foreach ($updateData as $tableName => $data) {
201 $tableName = $this->quotedTableMap[$tableName];
202 $versioned = $isVersioned && $versionedTable === $tableName;
203
204 $this->updateTable($entity, $tableName, $data, $versioned);
205 }
206
207 if ($this->class->requiresFetchAfterChange) {
208 // Make sure the table with the version column is updated even if no columns on that
209 // table were affected.
210 if ($isVersioned && ! isset($updateData[$versionedTable])) {
211 $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
212
213 $this->updateTable($entity, $tableName, [], true);
214 }
215
216 $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
217
218 $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers);
219 }
220 }
221
222 public function delete(object $entity): bool
223 {
224 $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
225 $id = array_combine($this->class->getIdentifierColumnNames(), $identifier);
226 $types = $this->getClassIdentifiersTypes($this->class);
227
228 $this->deleteJoinTableRecords($identifier, $types);
229
230 // Delete the row from the root table. Cascades do the rest.
231 $rootClass = $this->em->getClassMetadata($this->class->rootEntityName);
232 $rootTable = $this->quoteStrategy->getTableName($rootClass, $this->platform);
233 $rootTypes = $this->getClassIdentifiersTypes($rootClass);
234
235 return (bool) $this->conn->delete($rootTable, $id, $rootTypes);
236 }
237
238 public function getSelectSQL(
239 array|Criteria $criteria,
240 AssociationMapping|null $assoc = null,
241 LockMode|int|null $lockMode = null,
242 int|null $limit = null,
243 int|null $offset = null,
244 array|null $orderBy = null,
245 ): string {
246 $this->switchPersisterContext($offset, $limit);
247
248 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
249 $joinSql = $this->getJoinSql($baseTableAlias);
250
251 if ($assoc !== null && $assoc->isManyToMany()) {
252 $joinSql .= $this->getSelectManyToManyJoinSQL($assoc);
253 }
254
255 $conditionSql = $criteria instanceof Criteria
256 ? $this->getSelectConditionCriteriaSQL($criteria)
257 : $this->getSelectConditionSQL($criteria, $assoc);
258
259 $filterSql = $this->generateFilterConditionSQL(
260 $this->em->getClassMetadata($this->class->rootEntityName),
261 $this->getSQLTableAlias($this->class->rootEntityName),
262 );
263 // If the current class in the root entity, add the filters
264 if ($filterSql) {
265 $conditionSql .= $conditionSql
266 ? ' AND ' . $filterSql
267 : $filterSql;
268 }
269
270 $orderBySql = '';
271
272 if ($assoc !== null && $assoc->isOrdered()) {
273 $orderBy = $assoc->orderBy();
274 }
275
276 if ($orderBy) {
277 $orderBySql = $this->getOrderBySQL($orderBy, $baseTableAlias);
278 }
279
280 $lockSql = '';
281
282 switch ($lockMode) {
283 case LockMode::PESSIMISTIC_READ:
284 $lockSql = ' ' . $this->getReadLockSQL($this->platform);
285
286 break;
287
288 case LockMode::PESSIMISTIC_WRITE:
289 $lockSql = ' ' . $this->getWriteLockSQL($this->platform);
290
291 break;
292 }
293
294 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
295 $from = ' FROM ' . $tableName . ' ' . $baseTableAlias;
296 $where = $conditionSql !== '' ? ' WHERE ' . $conditionSql : '';
297 $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
298 $columnList = $this->getSelectColumnsSQL();
299 $query = 'SELECT ' . $columnList
300 . $lock
301 . $joinSql
302 . $where
303 . $orderBySql;
304
305 return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
306 }
307
308 public function getCountSQL(array|Criteria $criteria = []): string
309 {
310 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
311 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
312 $joinSql = $this->getJoinSql($baseTableAlias);
313
314 $conditionSql = $criteria instanceof Criteria
315 ? $this->getSelectConditionCriteriaSQL($criteria)
316 : $this->getSelectConditionSQL($criteria);
317
318 $filterSql = $this->generateFilterConditionSQL($this->em->getClassMetadata($this->class->rootEntityName), $this->getSQLTableAlias($this->class->rootEntityName));
319
320 if ($filterSql !== '') {
321 $conditionSql = $conditionSql
322 ? $conditionSql . ' AND ' . $filterSql
323 : $filterSql;
324 }
325
326 return 'SELECT COUNT(*) '
327 . 'FROM ' . $tableName . ' ' . $baseTableAlias
328 . $joinSql
329 . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
330 }
331
332 protected function getLockTablesSql(LockMode|int $lockMode): string
333 {
334 $joinSql = '';
335 $identifierColumns = $this->class->getIdentifierColumnNames();
336 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
337
338 // INNER JOIN parent tables
339 foreach ($this->class->parentClasses as $parentClassName) {
340 $conditions = [];
341 $tableAlias = $this->getSQLTableAlias($parentClassName);
342 $parentClass = $this->em->getClassMetadata($parentClassName);
343 $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
344
345 foreach ($identifierColumns as $idColumn) {
346 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
347 }
348
349 $joinSql .= implode(' AND ', $conditions);
350 }
351
352 return parent::getLockTablesSql($lockMode) . $joinSql;
353 }
354
355 /**
356 * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly.
357 */
358 protected function getSelectColumnsSQL(): string
359 {
360 // Create the column list fragment only once
361 if ($this->currentPersisterContext->selectColumnListSql !== null) {
362 return $this->currentPersisterContext->selectColumnListSql;
363 }
364
365 $columnList = [];
366 $discrColumn = $this->class->getDiscriminatorColumn();
367 $discrColumnName = $discrColumn->name;
368 $discrColumnType = $discrColumn->type;
369 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
370 $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName);
371
372 $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r');
373 $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
374 $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
375
376 // Add regular columns
377 foreach ($this->class->fieldMappings as $fieldName => $mapping) {
378 $class = isset($mapping->inherited)
379 ? $this->em->getClassMetadata($mapping->inherited)
380 : $this->class;
381
382 $columnList[] = $this->getSelectColumnSQL($fieldName, $class);
383 }
384
385 // Add foreign key columns
386 foreach ($this->class->associationMappings as $mapping) {
387 if (! $mapping->isToOneOwningSide()) {
388 continue;
389 }
390
391 $tableAlias = isset($mapping->inherited)
392 ? $this->getSQLTableAlias($mapping->inherited)
393 : $baseTableAlias;
394
395 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
396
397 foreach ($mapping->joinColumns as $joinColumn) {
398 $columnList[] = $this->getSelectJoinColumnSQL(
399 $tableAlias,
400 $joinColumn->name,
401 $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform),
402 PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em),
403 );
404 }
405 }
406
407 // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult).
408 $tableAlias = $this->class->rootEntityName === $this->class->name
409 ? $baseTableAlias
410 : $this->getSQLTableAlias($this->class->rootEntityName);
411
412 $columnList[] = $tableAlias . '.' . $discrColumnName;
413
414 // sub tables
415 foreach ($this->class->subClasses as $subClassName) {
416 $subClass = $this->em->getClassMetadata($subClassName);
417 $tableAlias = $this->getSQLTableAlias($subClassName);
418
419 // Add subclass columns
420 foreach ($subClass->fieldMappings as $fieldName => $mapping) {
421 if (isset($mapping->inherited)) {
422 continue;
423 }
424
425 $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
426 }
427
428 // Add join columns (foreign keys)
429 foreach ($subClass->associationMappings as $mapping) {
430 if (! $mapping->isToOneOwningSide() || isset($mapping->inherited)) {
431 continue;
432 }
433
434 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
435
436 foreach ($mapping->joinColumns as $joinColumn) {
437 $columnList[] = $this->getSelectJoinColumnSQL(
438 $tableAlias,
439 $joinColumn->name,
440 $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform),
441 PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em),
442 );
443 }
444 }
445 }
446
447 $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
448
449 return $this->currentPersisterContext->selectColumnListSql;
450 }
451
452 /**
453 * {@inheritDoc}
454 */
455 protected function getInsertColumnList(): array
456 {
457 // Identifier columns must always come first in the column list of subclasses.
458 $columns = $this->class->parentClasses
459 ? $this->class->getIdentifierColumnNames()
460 : [];
461
462 foreach ($this->class->reflFields as $name => $field) {
463 if (
464 isset($this->class->fieldMappings[$name]->inherited)
465 && ! isset($this->class->fieldMappings[$name]->id)
466 || isset($this->class->associationMappings[$name]->inherited)
467 || ($this->class->isVersioned && $this->class->versionField === $name)
468 || isset($this->class->embeddedClasses[$name])
469 || isset($this->class->fieldMappings[$name]->notInsertable)
470 ) {
471 continue;
472 }
473
474 if (isset($this->class->associationMappings[$name])) {
475 $assoc = $this->class->associationMappings[$name];
476 if ($assoc->isToOneOwningSide()) {
477 foreach ($assoc->targetToSourceKeyColumns as $sourceCol) {
478 $columns[] = $sourceCol;
479 }
480 }
481 } elseif (
482 $this->class->name !== $this->class->rootEntityName ||
483 ! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name
484 ) {
485 $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
486 $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type;
487 }
488 }
489
490 // Add discriminator column if it is the topmost class.
491 if ($this->class->name === $this->class->rootEntityName) {
492 $columns[] = $this->class->getDiscriminatorColumn()->name;
493 }
494
495 return $columns;
496 }
497
498 /**
499 * {@inheritDoc}
500 */
501 protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void
502 {
503 $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id);
504
505 foreach ($values as $field => $value) {
506 $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform);
507
508 $this->class->setFieldValue($entity, $field, $value);
509 }
510 }
511
512 /**
513 * {@inheritDoc}
514 */
515 protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed
516 {
517 $columnNames = [];
518 foreach ($this->class->fieldMappings as $key => $column) {
519 $class = null;
520 if ($this->class->isVersioned && $key === $versionedClass->versionField) {
521 $class = $versionedClass;
522 } elseif (isset($column->generated)) {
523 $class = isset($column->inherited)
524 ? $this->em->getClassMetadata($column->inherited)
525 : $this->class;
526 } else {
527 continue;
528 }
529
530 $columnNames[$key] = $this->getSelectColumnSQL($key, $class);
531 }
532
533 $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
534 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
535 $joinSql = $this->getJoinSql($baseTableAlias);
536 $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
537 foreach ($identifier as $i => $idValue) {
538 $identifier[$i] = $baseTableAlias . '.' . $idValue;
539 }
540
541 $sql = 'SELECT ' . implode(', ', $columnNames)
542 . ' FROM ' . $tableName . ' ' . $baseTableAlias
543 . $joinSql
544 . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
545
546 $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
547 $values = $this->conn->fetchNumeric(
548 $sql,
549 array_values($flatId),
550 $this->extractIdentifierTypes($id, $versionedClass),
551 );
552
553 if ($values === false) {
554 throw new LengthException('Unexpected empty result for database query.');
555 }
556
557 $values = array_combine(array_keys($columnNames), $values);
558
559 if (! $values) {
560 throw new LengthException('Unexpected number of database columns.');
561 }
562
563 return $values;
564 }
565
566 private function getJoinSql(string $baseTableAlias): string
567 {
568 $joinSql = '';
569 $identifierColumn = $this->class->getIdentifierColumnNames();
570
571 // INNER JOIN parent tables
572 foreach ($this->class->parentClasses as $parentClassName) {
573 $conditions = [];
574 $parentClass = $this->em->getClassMetadata($parentClassName);
575 $tableAlias = $this->getSQLTableAlias($parentClassName);
576 $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
577
578 foreach ($identifierColumn as $idColumn) {
579 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
580 }
581
582 $joinSql .= implode(' AND ', $conditions);
583 }
584
585 // OUTER JOIN sub tables
586 foreach ($this->class->subClasses as $subClassName) {
587 $conditions = [];
588 $subClass = $this->em->getClassMetadata($subClassName);
589 $tableAlias = $this->getSQLTableAlias($subClassName);
590 $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON ';
591
592 foreach ($identifierColumn as $idColumn) {
593 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
594 }
595
596 $joinSql .= implode(' AND ', $conditions);
597 }
598
599 return $joinSql;
600 }
601}
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php
new file mode 100644
index 0000000..4a4d999
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php
@@ -0,0 +1,166 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\ORM\Internal\SQLResultCasing;
9use Doctrine\ORM\Mapping\AssociationMapping;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Utility\PersisterHelper;
12
13use function array_flip;
14use function array_intersect;
15use function array_map;
16use function array_unshift;
17use function implode;
18use function strval;
19
20/**
21 * Persister for entities that participate in a hierarchy mapped with the
22 * SINGLE_TABLE strategy.
23 *
24 * @link https://martinfowler.com/eaaCatalog/singleTableInheritance.html
25 */
26class SingleTablePersister extends AbstractEntityInheritancePersister
27{
28 use SQLResultCasing;
29
30 protected function getDiscriminatorColumnTableName(): string
31 {
32 return $this->class->getTableName();
33 }
34
35 protected function getSelectColumnsSQL(): string
36 {
37 $columnList = [];
38 if ($this->currentPersisterContext->selectColumnListSql !== null) {
39 return $this->currentPersisterContext->selectColumnListSql;
40 }
41
42 $columnList[] = parent::getSelectColumnsSQL();
43
44 $rootClass = $this->em->getClassMetadata($this->class->rootEntityName);
45 $tableAlias = $this->getSQLTableAlias($rootClass->name);
46
47 // Append discriminator column
48 $discrColumn = $this->class->getDiscriminatorColumn();
49 $discrColumnName = $discrColumn->name;
50 $discrColumnType = $discrColumn->type;
51
52 $columnList[] = $tableAlias . '.' . $discrColumnName;
53
54 $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName);
55
56 $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
57 $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
58
59 // Append subclass columns
60 foreach ($this->class->subClasses as $subClassName) {
61 $subClass = $this->em->getClassMetadata($subClassName);
62
63 // Regular columns
64 foreach ($subClass->fieldMappings as $fieldName => $mapping) {
65 if (isset($mapping->inherited)) {
66 continue;
67 }
68
69 $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
70 }
71
72 // Foreign key columns
73 foreach ($subClass->associationMappings as $assoc) {
74 if (! $assoc->isToOneOwningSide() || isset($assoc->inherited)) {
75 continue;
76 }
77
78 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
79
80 foreach ($assoc->joinColumns as $joinColumn) {
81 $columnList[] = $this->getSelectJoinColumnSQL(
82 $tableAlias,
83 $joinColumn->name,
84 $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform),
85 PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em),
86 );
87 }
88 }
89 }
90
91 $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
92
93 return $this->currentPersisterContext->selectColumnListSql;
94 }
95
96 /**
97 * {@inheritDoc}
98 */
99 protected function getInsertColumnList(): array
100 {
101 $columns = parent::getInsertColumnList();
102
103 // Add discriminator column to the INSERT SQL
104 $columns[] = $this->class->getDiscriminatorColumn()->name;
105
106 return $columns;
107 }
108
109 protected function getSQLTableAlias(string $className, string $assocName = ''): string
110 {
111 return parent::getSQLTableAlias($this->class->rootEntityName, $assocName);
112 }
113
114 /**
115 * {@inheritDoc}
116 */
117 protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string
118 {
119 $conditionSql = parent::getSelectConditionSQL($criteria, $assoc);
120
121 if ($conditionSql) {
122 $conditionSql .= ' AND ';
123 }
124
125 return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL();
126 }
127
128 protected function getSelectConditionCriteriaSQL(Criteria $criteria): string
129 {
130 $conditionSql = parent::getSelectConditionCriteriaSQL($criteria);
131
132 if ($conditionSql) {
133 $conditionSql .= ' AND ';
134 }
135
136 return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL();
137 }
138
139 protected function getSelectConditionDiscriminatorValueSQL(): string
140 {
141 $values = array_map($this->conn->quote(...), array_map(
142 strval(...),
143 array_flip(array_intersect($this->class->discriminatorMap, $this->class->subClasses)),
144 ));
145
146 if ($this->class->discriminatorValue !== null) { // discriminators can be 0
147 array_unshift($values, $this->conn->quote((string) $this->class->discriminatorValue));
148 }
149
150 $discColumnName = $this->class->getDiscriminatorColumn()->name;
151
152 $values = implode(', ', $values);
153 $tableAlias = $this->getSQLTableAlias($this->class->name);
154
155 return $tableAlias . '.' . $discColumnName . ' IN (' . $values . ')';
156 }
157
158 protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string
159 {
160 // Ensure that the filters are applied to the root entity of the inheritance tree
161 $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName);
162 // we don't care about the $targetTableAlias, in a STI there is only one table.
163
164 return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias);
165 }
166}
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php
new file mode 100644
index 0000000..5c91312
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Exception;
6
7use Doctrine\ORM\Exception\PersisterException;
8
9class CantUseInOperatorOnCompositeKeys extends PersisterException
10{
11 public static function create(): self
12 {
13 return new self("Can't use IN operator on entities that have composite keys.");
14 }
15}
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php
new file mode 100644
index 0000000..7532800
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Exception;
6
7use Doctrine\ORM\Exception\PersisterException;
8
9class InvalidOrientation extends PersisterException
10{
11 public static function fromClassNameAndField(string $className, string $field): self
12 {
13 return new self('Invalid order by orientation specified for ' . $className . '#' . $field);
14 }
15}
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php
new file mode 100644
index 0000000..be7303e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Exception;
6
7use Doctrine\ORM\Exception\PersisterException;
8
9use function sprintf;
10
11final class UnrecognizedField extends PersisterException
12{
13 /** @deprecated Use {@see byFullyQualifiedName()} instead. */
14 public static function byName(string $field): self
15 {
16 return new self(sprintf('Unrecognized field: %s', $field));
17 }
18
19 /** @param class-string $className */
20 public static function byFullyQualifiedName(string $className, string $field): self
21 {
22 return new self(sprintf('Unrecognized field: %s::$%s', $className, $field));
23 }
24}
diff --git a/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php
new file mode 100644
index 0000000..4e7251e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php
@@ -0,0 +1,22 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters;
6
7use Doctrine\ORM\Exception\PersisterException;
8
9use function sprintf;
10
11final class MatchingAssociationFieldRequiresObject extends PersisterException
12{
13 public static function fromClassAndAssociation(string $class, string $associationName): self
14 {
15 return new self(sprintf(
16 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' .
17 'not compatible with matching on an in-memory collection, which compares objects by reference.',
18 $class,
19 $associationName,
20 ));
21 }
22}
diff --git a/vendor/doctrine/orm/src/Persisters/PersisterException.php b/vendor/doctrine/orm/src/Persisters/PersisterException.php
new file mode 100644
index 0000000..0016472
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/PersisterException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters;
6
7use Doctrine\ORM\Exception\ORMException;
8use Exception;
9
10use function sprintf;
11
12class PersisterException extends Exception implements ORMException
13{
14 public static function matchingAssocationFieldRequiresObject(string $class, string $associationName): PersisterException
15 {
16 return new self(sprintf(
17 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' .
18 'not compatible with matching on an in-memory collection, which compares objects by reference.',
19 $class,
20 $associationName,
21 ));
22 }
23}
diff --git a/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php
new file mode 100644
index 0000000..df60f6a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php
@@ -0,0 +1,79 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters;
6
7use Doctrine\Common\Collections\Expr\Comparison;
8use Doctrine\Common\Collections\Expr\CompositeExpression;
9use Doctrine\Common\Collections\Expr\ExpressionVisitor;
10use Doctrine\Common\Collections\Expr\Value;
11use Doctrine\ORM\Mapping\ClassMetadata;
12use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
13use RuntimeException;
14
15use function implode;
16use function in_array;
17use function is_object;
18
19/**
20 * Visit Expressions and generate SQL WHERE conditions from them.
21 */
22class SqlExpressionVisitor extends ExpressionVisitor
23{
24 public function __construct(
25 private readonly BasicEntityPersister $persister,
26 private readonly ClassMetadata $classMetadata,
27 ) {
28 }
29
30 /** Converts a comparison expression into the target query language output. */
31 public function walkComparison(Comparison $comparison): string
32 {
33 $field = $comparison->getField();
34 $value = $comparison->getValue()->getValue(); // shortcut for walkValue()
35
36 if (
37 isset($this->classMetadata->associationMappings[$field]) &&
38 $value !== null &&
39 ! is_object($value) &&
40 ! in_array($comparison->getOperator(), [Comparison::IN, Comparison::NIN], true)
41 ) {
42 throw MatchingAssociationFieldRequiresObject::fromClassAndAssociation(
43 $this->classMetadata->name,
44 $field,
45 );
46 }
47
48 return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator());
49 }
50
51 /**
52 * Converts a composite expression into the target query language output.
53 *
54 * @throws RuntimeException
55 */
56 public function walkCompositeExpression(CompositeExpression $expr): string
57 {
58 $expressionList = [];
59
60 foreach ($expr->getExpressionList() as $child) {
61 $expressionList[] = $this->dispatch($child);
62 }
63
64 return match ($expr->getType()) {
65 CompositeExpression::TYPE_AND => '(' . implode(' AND ', $expressionList) . ')',
66 CompositeExpression::TYPE_OR => '(' . implode(' OR ', $expressionList) . ')',
67 CompositeExpression::TYPE_NOT => 'NOT (' . $expressionList[0] . ')',
68 default => throw new RuntimeException('Unknown composite ' . $expr->getType()),
69 };
70 }
71
72 /**
73 * Converts a value expression into the target query language part.
74 */
75 public function walkValue(Value $value): string
76 {
77 return '?';
78 }
79}
diff --git a/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php
new file mode 100644
index 0000000..7f987ad
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters;
6
7use Doctrine\Common\Collections\Expr\Comparison;
8use Doctrine\Common\Collections\Expr\CompositeExpression;
9use Doctrine\Common\Collections\Expr\ExpressionVisitor;
10use Doctrine\Common\Collections\Expr\Value;
11
12/**
13 * Extract the values from a criteria/expression
14 */
15class SqlValueVisitor extends ExpressionVisitor
16{
17 /** @var mixed[] */
18 private array $values = [];
19
20 /** @var mixed[][] */
21 private array $types = [];
22
23 /**
24 * Converts a comparison expression into the target query language output.
25 *
26 * {@inheritDoc}
27 */
28 public function walkComparison(Comparison $comparison)
29 {
30 $value = $this->getValueFromComparison($comparison);
31
32 $this->values[] = $value;
33 $this->types[] = [$comparison->getField(), $value, $comparison->getOperator()];
34
35 return null;
36 }
37
38 /**
39 * Converts a composite expression into the target query language output.
40 *
41 * {@inheritDoc}
42 */
43 public function walkCompositeExpression(CompositeExpression $expr)
44 {
45 foreach ($expr->getExpressionList() as $child) {
46 $this->dispatch($child);
47 }
48
49 return null;
50 }
51
52 /**
53 * Converts a value expression into the target query language part.
54 *
55 * {@inheritDoc}
56 */
57 public function walkValue(Value $value)
58 {
59 return null;
60 }
61
62 /**
63 * Returns the Parameters and Types necessary for matching the last visited expression.
64 *
65 * @return mixed[][]
66 * @psalm-return array{0: array, 1: array<array<mixed>>}
67 */
68 public function getParamsAndTypes(): array
69 {
70 return [$this->values, $this->types];
71 }
72
73 /**
74 * Returns the value from a Comparison. In case of a CONTAINS comparison,
75 * the value is wrapped in %-signs, because it will be used in a LIKE clause.
76 */
77 protected function getValueFromComparison(Comparison $comparison): mixed
78 {
79 $value = $comparison->getValue()->getValue();
80
81 return match ($comparison->getOperator()) {
82 Comparison::CONTAINS => '%' . $value . '%',
83 Comparison::STARTS_WITH => $value . '%',
84 Comparison::ENDS_WITH => '%' . $value,
85 default => $value,
86 };
87 }
88}
diff --git a/vendor/doctrine/orm/src/PessimisticLockException.php b/vendor/doctrine/orm/src/PessimisticLockException.php
new file mode 100644
index 0000000..c71560f
--- /dev/null
+++ b/vendor/doctrine/orm/src/PessimisticLockException.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Exception\ORMException;
8use RuntimeException;
9
10class PessimisticLockException extends RuntimeException implements ORMException
11{
12 public static function lockFailed(): self
13 {
14 return new self('The pessimistic lock failed.');
15 }
16}
diff --git a/vendor/doctrine/orm/src/Proxy/Autoloader.php b/vendor/doctrine/orm/src/Proxy/Autoloader.php
new file mode 100644
index 0000000..1013e73
--- /dev/null
+++ b/vendor/doctrine/orm/src/Proxy/Autoloader.php
@@ -0,0 +1,86 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Proxy;
6
7use Closure;
8
9use function file_exists;
10use function ltrim;
11use function spl_autoload_register;
12use function str_replace;
13use function str_starts_with;
14use function strlen;
15use function substr;
16
17use const DIRECTORY_SEPARATOR;
18
19/**
20 * Special Autoloader for Proxy classes, which are not PSR-0 compliant.
21 */
22final class Autoloader
23{
24 /**
25 * Resolves proxy class name to a filename based on the following pattern.
26 *
27 * 1. Remove Proxy namespace from class name.
28 * 2. Remove namespace separators from remaining class name.
29 * 3. Return PHP filename from proxy-dir with the result from 2.
30 *
31 * @psalm-param class-string $className
32 *
33 * @throws NotAProxyClass
34 */
35 public static function resolveFile(string $proxyDir, string $proxyNamespace, string $className): string
36 {
37 if (! str_starts_with($className, $proxyNamespace)) {
38 throw new NotAProxyClass($className, $proxyNamespace);
39 }
40
41 // remove proxy namespace from class name
42 $classNameRelativeToProxyNamespace = substr($className, strlen($proxyNamespace));
43
44 // remove namespace separators from remaining class name
45 $fileName = str_replace('\\', '', $classNameRelativeToProxyNamespace);
46
47 return $proxyDir . DIRECTORY_SEPARATOR . $fileName . '.php';
48 }
49
50 /**
51 * Registers and returns autoloader callback for the given proxy dir and namespace.
52 *
53 * @param Closure(string, string, class-string): void|null $notFoundCallback Invoked when the proxy file is not found.
54 *
55 * @return Closure(string): void
56 */
57 public static function register(
58 string $proxyDir,
59 string $proxyNamespace,
60 Closure|null $notFoundCallback = null,
61 ): Closure {
62 $proxyNamespace = ltrim($proxyNamespace, '\\');
63
64 $autoloader = /** @param class-string $className */ static function (string $className) use ($proxyDir, $proxyNamespace, $notFoundCallback): void {
65 if ($proxyNamespace === '') {
66 return;
67 }
68
69 if (! str_starts_with($className, $proxyNamespace)) {
70 return;
71 }
72
73 $file = Autoloader::resolveFile($proxyDir, $proxyNamespace, $className);
74
75 if ($notFoundCallback && ! file_exists($file)) {
76 $notFoundCallback($proxyDir, $proxyNamespace, $className);
77 }
78
79 require $file;
80 };
81
82 spl_autoload_register($autoloader);
83
84 return $autoloader;
85 }
86}
diff --git a/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php b/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php
new file mode 100644
index 0000000..1345f2e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Proxy;
6
7use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
8use Doctrine\Persistence\Proxy;
9
10use function strrpos;
11use function substr;
12
13/**
14 * Class-related functionality for objects that might or not be proxy objects
15 * at the moment.
16 */
17final class DefaultProxyClassNameResolver implements ProxyClassNameResolver
18{
19 public function resolveClassName(string $className): string
20 {
21 $pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
22
23 if ($pos === false) {
24 return $className;
25 }
26
27 return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
28 }
29
30 /** @return class-string */
31 public static function getClass(object $object): string
32 {
33 return (new self())->resolveClassName($object::class);
34 }
35}
diff --git a/vendor/doctrine/orm/src/Proxy/InternalProxy.php b/vendor/doctrine/orm/src/Proxy/InternalProxy.php
new file mode 100644
index 0000000..7c1d833
--- /dev/null
+++ b/vendor/doctrine/orm/src/Proxy/InternalProxy.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Proxy;
6
7use Doctrine\Persistence\Proxy;
8
9/**
10 * @internal
11 *
12 * @template T of object
13 * @template-extends Proxy<T>
14 */
15interface InternalProxy extends Proxy
16{
17 public function __setInitialized(bool $initialized): void;
18}
diff --git a/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php b/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php
new file mode 100644
index 0000000..689cc3e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php
@@ -0,0 +1,22 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Proxy;
6
7use Doctrine\ORM\Exception\ORMException;
8use InvalidArgumentException;
9
10use function sprintf;
11
12final class NotAProxyClass extends InvalidArgumentException implements ORMException
13{
14 public function __construct(string $className, string $proxyNamespace)
15 {
16 parent::__construct(sprintf(
17 'The class "%s" is not part of the proxy namespace "%s"',
18 $className,
19 $proxyNamespace,
20 ));
21 }
22}
diff --git a/vendor/doctrine/orm/src/Proxy/ProxyFactory.php b/vendor/doctrine/orm/src/Proxy/ProxyFactory.php
new file mode 100644
index 0000000..b2d114a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Proxy/ProxyFactory.php
@@ -0,0 +1,439 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Proxy;
6
7use Closure;
8use Doctrine\ORM\EntityManagerInterface;
9use Doctrine\ORM\EntityNotFoundException;
10use Doctrine\ORM\ORMInvalidArgumentException;
11use Doctrine\ORM\Persisters\Entity\EntityPersister;
12use Doctrine\ORM\UnitOfWork;
13use Doctrine\ORM\Utility\IdentifierFlattener;
14use Doctrine\Persistence\Mapping\ClassMetadata;
15use Doctrine\Persistence\Proxy;
16use ReflectionProperty;
17use Symfony\Component\VarExporter\ProxyHelper;
18
19use function array_combine;
20use function array_flip;
21use function array_intersect_key;
22use function assert;
23use function bin2hex;
24use function chmod;
25use function class_exists;
26use function dirname;
27use function file_exists;
28use function file_put_contents;
29use function filemtime;
30use function is_bool;
31use function is_dir;
32use function is_int;
33use function is_writable;
34use function ltrim;
35use function mkdir;
36use function preg_match_all;
37use function random_bytes;
38use function rename;
39use function rtrim;
40use function str_replace;
41use function strpos;
42use function strrpos;
43use function strtr;
44use function substr;
45use function ucfirst;
46
47use const DIRECTORY_SEPARATOR;
48
49/**
50 * This factory is used to create proxy objects for entities at runtime.
51 */
52class ProxyFactory
53{
54 /**
55 * Never autogenerate a proxy and rely that it was generated by some
56 * process before deployment.
57 */
58 public const AUTOGENERATE_NEVER = 0;
59
60 /**
61 * Always generates a new proxy in every request.
62 *
63 * This is only sane during development.
64 */
65 public const AUTOGENERATE_ALWAYS = 1;
66
67 /**
68 * Autogenerate the proxy class when the proxy file does not exist.
69 *
70 * This strategy causes a file_exists() call whenever any proxy is used the
71 * first time in a request.
72 */
73 public const AUTOGENERATE_FILE_NOT_EXISTS = 2;
74
75 /**
76 * Generate the proxy classes using eval().
77 *
78 * This strategy is only sane for development, and even then it gives me
79 * the creeps a little.
80 */
81 public const AUTOGENERATE_EVAL = 3;
82
83 /**
84 * Autogenerate the proxy class when the proxy file does not exist or
85 * when the proxied file changed.
86 *
87 * This strategy causes a file_exists() call whenever any proxy is used the
88 * first time in a request. When the proxied file is changed, the proxy will
89 * be updated.
90 */
91 public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4;
92
93 private const PROXY_CLASS_TEMPLATE = <<<'EOPHP'
94<?php
95
96namespace <namespace>;
97
98/**
99 * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR
100 */
101class <proxyShortClassName> extends \<className> implements \<baseProxyInterface>
102{
103 <useLazyGhostTrait>
104
105 public function __isInitialized(): bool
106 {
107 return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
108 }
109
110 public function __serialize(): array
111 {
112 <serializeImpl>
113 }
114}
115
116EOPHP;
117
118 /** The UnitOfWork this factory uses to retrieve persisters */
119 private readonly UnitOfWork $uow;
120
121 /** @var self::AUTOGENERATE_* */
122 private $autoGenerate;
123
124 /** The IdentifierFlattener used for manipulating identifiers */
125 private readonly IdentifierFlattener $identifierFlattener;
126
127 /** @var array<class-string, Closure> */
128 private array $proxyFactories = [];
129
130 /**
131 * Initializes a new instance of the <tt>ProxyFactory</tt> class that is
132 * connected to the given <tt>EntityManager</tt>.
133 *
134 * @param EntityManagerInterface $em The EntityManager the new factory works for.
135 * @param string $proxyDir The directory to use for the proxy classes. It must exist.
136 * @param string $proxyNs The namespace to use for the proxy classes.
137 * @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes.
138 */
139 public function __construct(
140 private readonly EntityManagerInterface $em,
141 private readonly string $proxyDir,
142 private readonly string $proxyNs,
143 bool|int $autoGenerate = self::AUTOGENERATE_NEVER,
144 ) {
145 if (! $proxyDir) {
146 throw ORMInvalidArgumentException::proxyDirectoryRequired();
147 }
148
149 if (! $proxyNs) {
150 throw ORMInvalidArgumentException::proxyNamespaceRequired();
151 }
152
153 if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) {
154 throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate);
155 }
156
157 $this->uow = $em->getUnitOfWork();
158 $this->autoGenerate = (int) $autoGenerate;
159 $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
160 }
161
162 /**
163 * @param class-string $className
164 * @param array<mixed> $identifier
165 */
166 public function getProxy(string $className, array $identifier): InternalProxy
167 {
168 $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className);
169
170 return $proxyFactory($identifier);
171 }
172
173 /**
174 * Generates proxy classes for all given classes.
175 *
176 * @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies.
177 * @param string|null $proxyDir The target directory of the proxy classes. If not specified, the
178 * directory configured on the Configuration of the EntityManager used
179 * by this factory is used.
180 *
181 * @return int Number of generated proxies.
182 */
183 public function generateProxyClasses(array $classes, string|null $proxyDir = null): int
184 {
185 $generated = 0;
186
187 foreach ($classes as $class) {
188 if ($this->skipClass($class)) {
189 continue;
190 }
191
192 $proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir);
193 $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
194
195 $this->generateProxyClass($class, $proxyFileName, $proxyClassName);
196
197 ++$generated;
198 }
199
200 return $generated;
201 }
202
203 protected function skipClass(ClassMetadata $metadata): bool
204 {
205 return $metadata->isMappedSuperclass
206 || $metadata->isEmbeddedClass
207 || $metadata->getReflectionClass()->isAbstract();
208 }
209
210 /**
211 * Creates a closure capable of initializing a proxy
212 *
213 * @return Closure(InternalProxy, array):void
214 *
215 * @throws EntityNotFoundException
216 */
217 private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
218 {
219 return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
220 $original = $entityPersister->loadById($identifier);
221
222 if ($original === null) {
223 throw EntityNotFoundException::fromClassNameAndIdentifier(
224 $classMetadata->getName(),
225 $identifierFlattener->flattenIdentifier($classMetadata, $identifier),
226 );
227 }
228
229 if ($proxy === $original) {
230 return;
231 }
232
233 $class = $entityPersister->getClassMetadata();
234
235 foreach ($class->getReflectionProperties() as $property) {
236 if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
237 continue;
238 }
239
240 $property->setValue($proxy, $property->getValue($original));
241 }
242 };
243 }
244
245 private function getProxyFileName(string $className, string $baseDirectory): string
246 {
247 $baseDirectory = $baseDirectory ?: $this->proxyDir;
248
249 return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER
250 . str_replace('\\', '', $className) . '.php';
251 }
252
253 private function getProxyFactory(string $className): Closure
254 {
255 $skippedProperties = [];
256 $class = $this->em->getClassMetadata($className);
257 $identifiers = array_flip($class->getIdentifierFieldNames());
258 $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
259 $reflector = $class->getReflectionClass();
260
261 while ($reflector) {
262 foreach ($reflector->getProperties($filter) as $property) {
263 $name = $property->name;
264
265 if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
266 continue;
267 }
268
269 $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : '');
270
271 $skippedProperties[$prefix . $name] = true;
272 }
273
274 $filter = ReflectionProperty::IS_PRIVATE;
275 $reflector = $reflector->getParentClass();
276 }
277
278 $className = $class->getName(); // aliases and case sensitivity
279 $entityPersister = $this->uow->getEntityPersister($className);
280 $initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener);
281 $proxyClassName = $this->loadProxyClass($class);
282 $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
283
284 $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
285 $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
286 $initializer($object, $identifier);
287 }, $skippedProperties);
288
289 foreach ($identifierFields as $idField => $reflector) {
290 if (! isset($identifier[$idField])) {
291 throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField);
292 }
293
294 assert($reflector !== null);
295 $reflector->setValue($proxy, $identifier[$idField]);
296 }
297
298 return $proxy;
299 }, null, $proxyClassName);
300
301 return $this->proxyFactories[$className] = $proxyFactory;
302 }
303
304 private function loadProxyClass(ClassMetadata $class): string
305 {
306 $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs);
307
308 if (class_exists($proxyClassName, false)) {
309 return $proxyClassName;
310 }
311
312 if ($this->autoGenerate === self::AUTOGENERATE_EVAL) {
313 $this->generateProxyClass($class, null, $proxyClassName);
314
315 return $proxyClassName;
316 }
317
318 $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir);
319
320 switch ($this->autoGenerate) {
321 case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED:
322 if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) {
323 break;
324 }
325 // no break
326 case self::AUTOGENERATE_FILE_NOT_EXISTS:
327 if (file_exists($fileName)) {
328 break;
329 }
330 // no break
331 case self::AUTOGENERATE_ALWAYS:
332 $this->generateProxyClass($class, $fileName, $proxyClassName);
333 break;
334 }
335
336 require $fileName;
337
338 return $proxyClassName;
339 }
340
341 private function generateProxyClass(ClassMetadata $class, string|null $fileName, string $proxyClassName): void
342 {
343 $i = strrpos($proxyClassName, '\\');
344 $placeholders = [
345 '<className>' => $class->getName(),
346 '<namespace>' => substr($proxyClassName, 0, $i),
347 '<proxyShortClassName>' => substr($proxyClassName, 1 + $i),
348 '<baseProxyInterface>' => InternalProxy::class,
349 ];
350
351 preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches);
352
353 foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) {
354 $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class);
355 }
356
357 $proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders);
358
359 if (! $fileName) {
360 if (! class_exists($proxyClassName)) {
361 eval(substr($proxyCode, 5));
362 }
363
364 return;
365 }
366
367 $parentDirectory = dirname($fileName);
368
369 if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) {
370 throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
371 }
372
373 if (! is_writable($parentDirectory)) {
374 throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir);
375 }
376
377 $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12));
378
379 file_put_contents($tmpFileName, $proxyCode);
380 @chmod($tmpFileName, 0664);
381 rename($tmpFileName, $fileName);
382 }
383
384 private function generateUseLazyGhostTrait(ClassMetadata $class): string
385 {
386 $code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
387 $code = substr($code, 7 + (int) strpos($code, "\n{"));
388 $code = substr($code, 0, (int) strpos($code, "\n}"));
389 $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
390 initializeLazyObject as private;
391 setLazyObjectAsInitialized as public __setInitialized;
392 isLazyObjectInitialized as private;
393 createLazyGhost as private;
394 resetLazyObject as private;
395 }
396
397 public function __load(): void
398 {
399 $this->initializeLazyObject();
400 }
401 '), $code);
402
403 return $code;
404 }
405
406 private function generateSerializeImpl(ClassMetadata $class): string
407 {
408 $reflector = $class->getReflectionClass();
409 $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this';
410
411 $code = '$properties = ' . $properties . ';
412 unset($properties["\0" . self::class . "\0lazyObjectState"]);
413
414 ';
415
416 if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) {
417 return $code . 'return $properties;';
418 }
419
420 return $code . '$data = [];
421
422 foreach (parent::__sleep() as $name) {
423 $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null;
424
425 if (null === $k) {
426 trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE);
427 } else {
428 $data[$k] = $value;
429 }
430 }
431
432 return $data;';
433 }
434
435 private static function generateProxyClassName(string $className, string $proxyNamespace): string
436 {
437 return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\');
438 }
439}
diff --git a/vendor/doctrine/orm/src/Query.php b/vendor/doctrine/orm/src/Query.php
new file mode 100644
index 0000000..a869316
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query.php
@@ -0,0 +1,682 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\DBAL\LockMode;
8use Doctrine\DBAL\Result;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Query\AST\DeleteStatement;
12use Doctrine\ORM\Query\AST\SelectStatement;
13use Doctrine\ORM\Query\AST\UpdateStatement;
14use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
15use Doctrine\ORM\Query\Parameter;
16use Doctrine\ORM\Query\ParameterTypeInferer;
17use Doctrine\ORM\Query\Parser;
18use Doctrine\ORM\Query\ParserResult;
19use Doctrine\ORM\Query\QueryException;
20use Doctrine\ORM\Query\ResultSetMapping;
21use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
22use Psr\Cache\CacheItemPoolInterface;
23
24use function array_keys;
25use function array_values;
26use function assert;
27use function count;
28use function get_debug_type;
29use function in_array;
30use function ksort;
31use function md5;
32use function reset;
33use function serialize;
34use function sha1;
35use function stripos;
36
37/**
38 * A Query object represents a DQL query.
39 *
40 * @final
41 */
42class Query extends AbstractQuery
43{
44 /**
45 * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
46 */
47 public const STATE_CLEAN = 1;
48
49 /**
50 * A query object is in state DIRTY when it has DQL parts that have not yet been
51 * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart
52 * is called.
53 */
54 public const STATE_DIRTY = 2;
55
56 /* Query HINTS */
57
58 /**
59 * The refresh hint turns any query into a refresh query with the result that
60 * any local changes in entities are overridden with the fetched values.
61 */
62 public const HINT_REFRESH = 'doctrine.refresh';
63
64 public const HINT_CACHE_ENABLED = 'doctrine.cache.enabled';
65
66 public const HINT_CACHE_EVICT = 'doctrine.cache.evict';
67
68 /**
69 * Internal hint: is set to the proxy entity that is currently triggered for loading
70 */
71 public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
72
73 /**
74 * The includeMetaColumns query hint causes meta columns like foreign keys and
75 * discriminator columns to be selected and returned as part of the query result.
76 *
77 * This hint does only apply to non-object queries.
78 */
79 public const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns';
80
81 /**
82 * An array of class names that implement \Doctrine\ORM\Query\TreeWalker and
83 * are iterated and executed after the DQL has been parsed into an AST.
84 */
85 public const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers';
86
87 /**
88 * A string with a class name that implements \Doctrine\ORM\Query\TreeWalker
89 * and is used for generating the target SQL from any DQL AST tree.
90 */
91 public const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker';
92
93 /**
94 * Marks queries as creating only read only objects.
95 *
96 * If the object retrieved from the query is already in the identity map
97 * then it does not get marked as read only if it wasn't already.
98 */
99 public const HINT_READ_ONLY = 'doctrine.readOnly';
100
101 public const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';
102
103 public const HINT_LOCK_MODE = 'doctrine.lockMode';
104
105 /**
106 * The current state of this query.
107 *
108 * @psalm-var self::STATE_*
109 */
110 private int $state = self::STATE_DIRTY;
111
112 /**
113 * A snapshot of the parameter types the query was parsed with.
114 *
115 * @var array<string,Type>
116 */
117 private array $parsedTypes = [];
118
119 /**
120 * Cached DQL query.
121 */
122 private string|null $dql = null;
123
124 /**
125 * The parser result that holds DQL => SQL information.
126 */
127 private ParserResult $parserResult;
128
129 /**
130 * The first result to return (the "offset").
131 */
132 private int $firstResult = 0;
133
134 /**
135 * The maximum number of results to return (the "limit").
136 */
137 private int|null $maxResults = null;
138
139 /**
140 * The cache driver used for caching queries.
141 */
142 private CacheItemPoolInterface|null $queryCache = null;
143
144 /**
145 * Whether or not expire the query cache.
146 */
147 private bool $expireQueryCache = false;
148
149 /**
150 * The query cache lifetime.
151 */
152 private int|null $queryCacheTTL = null;
153
154 /**
155 * Whether to use a query cache, if available. Defaults to TRUE.
156 */
157 private bool $useQueryCache = true;
158
159 /**
160 * Gets the SQL query/queries that correspond to this DQL query.
161 *
162 * @return list<string>|string The built sql query or an array of all sql queries.
163 */
164 public function getSQL(): string|array
165 {
166 return $this->parse()->getSqlExecutor()->getSqlStatements();
167 }
168
169 /**
170 * Returns the corresponding AST for this DQL query.
171 */
172 public function getAST(): SelectStatement|UpdateStatement|DeleteStatement
173 {
174 $parser = new Parser($this);
175
176 return $parser->getAST();
177 }
178
179 protected function getResultSetMapping(): ResultSetMapping
180 {
181 // parse query or load from cache
182 if ($this->resultSetMapping === null) {
183 $this->resultSetMapping = $this->parse()->getResultSetMapping();
184 }
185
186 return $this->resultSetMapping;
187 }
188
189 /**
190 * Parses the DQL query, if necessary, and stores the parser result.
191 *
192 * Note: Populates $this->_parserResult as a side-effect.
193 */
194 private function parse(): ParserResult
195 {
196 $types = [];
197
198 foreach ($this->parameters as $parameter) {
199 /** @var Query\Parameter $parameter */
200 $types[$parameter->getName()] = $parameter->getType();
201 }
202
203 // Return previous parser result if the query and the filter collection are both clean
204 if ($this->state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->em->isFiltersStateClean()) {
205 return $this->parserResult;
206 }
207
208 $this->state = self::STATE_CLEAN;
209 $this->parsedTypes = $types;
210
211 $queryCache = $this->queryCache ?? $this->em->getConfiguration()->getQueryCache();
212 // Check query cache.
213 if (! ($this->useQueryCache && $queryCache)) {
214 $parser = new Parser($this);
215
216 $this->parserResult = $parser->parse();
217
218 return $this->parserResult;
219 }
220
221 $cacheItem = $queryCache->getItem($this->getQueryCacheId());
222
223 if (! $this->expireQueryCache && $cacheItem->isHit()) {
224 $cached = $cacheItem->get();
225 if ($cached instanceof ParserResult) {
226 // Cache hit.
227 $this->parserResult = $cached;
228
229 return $this->parserResult;
230 }
231 }
232
233 // Cache miss.
234 $parser = new Parser($this);
235
236 $this->parserResult = $parser->parse();
237
238 $queryCache->save($cacheItem->set($this->parserResult)->expiresAfter($this->queryCacheTTL));
239
240 return $this->parserResult;
241 }
242
243 protected function _doExecute(): Result|int
244 {
245 $executor = $this->parse()->getSqlExecutor();
246
247 if ($this->queryCacheProfile) {
248 $executor->setQueryCacheProfile($this->queryCacheProfile);
249 } else {
250 $executor->removeQueryCacheProfile();
251 }
252
253 if ($this->resultSetMapping === null) {
254 $this->resultSetMapping = $this->parserResult->getResultSetMapping();
255 }
256
257 // Prepare parameters
258 $paramMappings = $this->parserResult->getParameterMappings();
259 $paramCount = count($this->parameters);
260 $mappingCount = count($paramMappings);
261
262 if ($paramCount > $mappingCount) {
263 throw QueryException::tooManyParameters($mappingCount, $paramCount);
264 }
265
266 if ($paramCount < $mappingCount) {
267 throw QueryException::tooFewParameters($mappingCount, $paramCount);
268 }
269
270 // evict all cache for the entity region
271 if ($this->hasCache && isset($this->hints[self::HINT_CACHE_EVICT]) && $this->hints[self::HINT_CACHE_EVICT]) {
272 $this->evictEntityCacheRegion();
273 }
274
275 [$sqlParams, $types] = $this->processParameterMappings($paramMappings);
276
277 $this->evictResultSetCache(
278 $executor,
279 $sqlParams,
280 $types,
281 $this->em->getConnection()->getParams(),
282 );
283
284 return $executor->execute($this->em->getConnection(), $sqlParams, $types);
285 }
286
287 /**
288 * @param array<string,mixed> $sqlParams
289 * @param array<string,Type> $types
290 * @param array<string,mixed> $connectionParams
291 */
292 private function evictResultSetCache(
293 AbstractSqlExecutor $executor,
294 array $sqlParams,
295 array $types,
296 array $connectionParams,
297 ): void {
298 if ($this->queryCacheProfile === null || ! $this->getExpireResultCache()) {
299 return;
300 }
301
302 $cache = $this->queryCacheProfile->getResultCache();
303
304 assert($cache !== null);
305
306 $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array
307
308 foreach ($statements as $statement) {
309 $cacheKeys = $this->queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams);
310 $cache->deleteItem(reset($cacheKeys));
311 }
312 }
313
314 /**
315 * Evict entity cache region
316 */
317 private function evictEntityCacheRegion(): void
318 {
319 $AST = $this->getAST();
320
321 if ($AST instanceof SelectStatement) {
322 throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.');
323 }
324
325 $className = $AST instanceof DeleteStatement
326 ? $AST->deleteClause->abstractSchemaName
327 : $AST->updateClause->abstractSchemaName;
328
329 $this->em->getCache()->evictEntityRegion($className);
330 }
331
332 /**
333 * Processes query parameter mappings.
334 *
335 * @param array<list<int>> $paramMappings
336 *
337 * @return mixed[][]
338 * @psalm-return array{0: list<mixed>, 1: array}
339 *
340 * @throws Query\QueryException
341 */
342 private function processParameterMappings(array $paramMappings): array
343 {
344 $sqlParams = [];
345 $types = [];
346
347 foreach ($this->parameters as $parameter) {
348 $key = $parameter->getName();
349
350 if (! isset($paramMappings[$key])) {
351 throw QueryException::unknownParameter($key);
352 }
353
354 [$value, $type] = $this->resolveParameterValue($parameter);
355
356 foreach ($paramMappings[$key] as $position) {
357 $types[$position] = $type;
358 }
359
360 $sqlPositions = $paramMappings[$key];
361
362 // optimized multi value sql positions away for now,
363 // they are not allowed in DQL anyways.
364 $value = [$value];
365 $countValue = count($value);
366
367 for ($i = 0, $l = count($sqlPositions); $i < $l; $i++) {
368 $sqlParams[$sqlPositions[$i]] = $value[$i % $countValue];
369 }
370 }
371
372 if (count($sqlParams) !== count($types)) {
373 throw QueryException::parameterTypeMismatch();
374 }
375
376 if ($sqlParams) {
377 ksort($sqlParams);
378 $sqlParams = array_values($sqlParams);
379
380 ksort($types);
381 $types = array_values($types);
382 }
383
384 return [$sqlParams, $types];
385 }
386
387 /**
388 * @return mixed[] tuple of (value, type)
389 * @psalm-return array{0: mixed, 1: mixed}
390 */
391 private function resolveParameterValue(Parameter $parameter): array
392 {
393 if ($parameter->typeWasSpecified()) {
394 return [$parameter->getValue(), $parameter->getType()];
395 }
396
397 $key = $parameter->getName();
398 $originalValue = $parameter->getValue();
399 $value = $originalValue;
400 $rsm = $this->getResultSetMapping();
401
402 if ($value instanceof ClassMetadata && isset($rsm->metadataParameterMapping[$key])) {
403 $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]);
404 }
405
406 if ($value instanceof ClassMetadata && isset($rsm->discriminatorParameters[$key])) {
407 $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->em));
408 }
409
410 $processedValue = $this->processParameterValue($value);
411
412 return [
413 $processedValue,
414 $originalValue === $processedValue
415 ? $parameter->getType()
416 : ParameterTypeInferer::inferType($processedValue),
417 ];
418 }
419
420 /**
421 * Defines a cache driver to be used for caching queries.
422 *
423 * @return $this
424 */
425 public function setQueryCache(CacheItemPoolInterface|null $queryCache): self
426 {
427 $this->queryCache = $queryCache;
428
429 return $this;
430 }
431
432 /**
433 * Defines whether the query should make use of a query cache, if available.
434 *
435 * @return $this
436 */
437 public function useQueryCache(bool $bool): self
438 {
439 $this->useQueryCache = $bool;
440
441 return $this;
442 }
443
444 /**
445 * Defines how long the query cache will be active before expire.
446 *
447 * @param int|null $timeToLive How long the cache entry is valid.
448 *
449 * @return $this
450 */
451 public function setQueryCacheLifetime(int|null $timeToLive): self
452 {
453 $this->queryCacheTTL = $timeToLive;
454
455 return $this;
456 }
457
458 /**
459 * Retrieves the lifetime of resultset cache.
460 */
461 public function getQueryCacheLifetime(): int|null
462 {
463 return $this->queryCacheTTL;
464 }
465
466 /**
467 * Defines if the query cache is active or not.
468 *
469 * @return $this
470 */
471 public function expireQueryCache(bool $expire = true): self
472 {
473 $this->expireQueryCache = $expire;
474
475 return $this;
476 }
477
478 /**
479 * Retrieves if the query cache is active or not.
480 */
481 public function getExpireQueryCache(): bool
482 {
483 return $this->expireQueryCache;
484 }
485
486 public function free(): void
487 {
488 parent::free();
489
490 $this->dql = null;
491 $this->state = self::STATE_CLEAN;
492 }
493
494 /**
495 * Sets a DQL query string.
496 */
497 public function setDQL(string $dqlQuery): self
498 {
499 $this->dql = $dqlQuery;
500 $this->state = self::STATE_DIRTY;
501
502 return $this;
503 }
504
505 /**
506 * Returns the DQL query that is represented by this query object.
507 */
508 public function getDQL(): string|null
509 {
510 return $this->dql;
511 }
512
513 /**
514 * Returns the state of this query object
515 * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL
516 * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY.
517 *
518 * @see AbstractQuery::STATE_CLEAN
519 * @see AbstractQuery::STATE_DIRTY
520 *
521 * @return int The query state.
522 * @psalm-return self::STATE_* The query state.
523 */
524 public function getState(): int
525 {
526 return $this->state;
527 }
528
529 /**
530 * Method to check if an arbitrary piece of DQL exists
531 *
532 * @param string $dql Arbitrary piece of DQL to check for.
533 */
534 public function contains(string $dql): bool
535 {
536 return stripos($this->getDQL(), $dql) !== false;
537 }
538
539 /**
540 * Sets the position of the first result to retrieve (the "offset").
541 *
542 * @param int $firstResult The first result to return.
543 *
544 * @return $this
545 */
546 public function setFirstResult(int $firstResult): self
547 {
548 $this->firstResult = $firstResult;
549 $this->state = self::STATE_DIRTY;
550
551 return $this;
552 }
553
554 /**
555 * Gets the position of the first result the query object was set to retrieve (the "offset").
556 * Returns 0 if {@link setFirstResult} was not applied to this query.
557 *
558 * @return int The position of the first result.
559 */
560 public function getFirstResult(): int
561 {
562 return $this->firstResult;
563 }
564
565 /**
566 * Sets the maximum number of results to retrieve (the "limit").
567 *
568 * @return $this
569 */
570 public function setMaxResults(int|null $maxResults): self
571 {
572 $this->maxResults = $maxResults;
573 $this->state = self::STATE_DIRTY;
574
575 return $this;
576 }
577
578 /**
579 * Gets the maximum number of results the query object was set to retrieve (the "limit").
580 * Returns NULL if {@link setMaxResults} was not applied to this query.
581 *
582 * @return int|null Maximum number of results.
583 */
584 public function getMaxResults(): int|null
585 {
586 return $this->maxResults;
587 }
588
589 /** {@inheritDoc} */
590 public function toIterable(iterable $parameters = [], $hydrationMode = self::HYDRATE_OBJECT): iterable
591 {
592 $this->setHint(self::HINT_INTERNAL_ITERATION, true);
593
594 return parent::toIterable($parameters, $hydrationMode);
595 }
596
597 public function setHint(string $name, mixed $value): static
598 {
599 $this->state = self::STATE_DIRTY;
600
601 return parent::setHint($name, $value);
602 }
603
604 public function setHydrationMode(string|int $hydrationMode): static
605 {
606 $this->state = self::STATE_DIRTY;
607
608 return parent::setHydrationMode($hydrationMode);
609 }
610
611 /**
612 * Set the lock mode for this Query.
613 *
614 * @see \Doctrine\DBAL\LockMode
615 *
616 * @psalm-param LockMode::* $lockMode
617 *
618 * @return $this
619 *
620 * @throws TransactionRequiredException
621 */
622 public function setLockMode(LockMode|int $lockMode): self
623 {
624 if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) {
625 if (! $this->em->getConnection()->isTransactionActive()) {
626 throw TransactionRequiredException::transactionRequired();
627 }
628 }
629
630 $this->setHint(self::HINT_LOCK_MODE, $lockMode);
631
632 return $this;
633 }
634
635 /**
636 * Get the current lock mode for this query.
637 *
638 * @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set.
639 * @psalm-return LockMode::*|null
640 */
641 public function getLockMode(): LockMode|int|null
642 {
643 $lockMode = $this->getHint(self::HINT_LOCK_MODE);
644
645 if ($lockMode === false) {
646 return null;
647 }
648
649 return $lockMode;
650 }
651
652 /**
653 * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
654 */
655 protected function getQueryCacheId(): string
656 {
657 ksort($this->hints);
658
659 return md5(
660 $this->getDQL() . serialize($this->hints) .
661 '&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
662 ($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') .
663 '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
664 '&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT',
665 );
666 }
667
668 protected function getHash(): string
669 {
670 return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults);
671 }
672
673 /**
674 * Cleanup Query resource when clone is called.
675 */
676 public function __clone()
677 {
678 parent::__clone();
679
680 $this->state = self::STATE_DIRTY;
681 }
682}
diff --git a/vendor/doctrine/orm/src/Query/AST/ASTException.php b/vendor/doctrine/orm/src/Query/AST/ASTException.php
new file mode 100644
index 0000000..1ef890a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ASTException.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\QueryException;
8
9use function get_debug_type;
10
11/**
12 * Base exception class for AST exceptions.
13 */
14class ASTException extends QueryException
15{
16 public static function noDispatchForNode(Node $node): self
17 {
18 return new self('Double-dispatch for node ' . get_debug_type($node) . ' is not supported.');
19 }
20}
diff --git a/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php
new file mode 100644
index 0000000..468c65c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class AggregateExpression extends Node
10{
11 /** @param bool $isDistinct Some aggregate expressions support distinct, eg COUNT. */
12 public function __construct(
13 public string $functionName,
14 public Node|string $pathExpression,
15 public bool $isDistinct,
16 ) {
17 }
18
19 public function dispatch(SqlWalker $walker): string
20 {
21 return $walker->walkAggregateExpression($this);
22 }
23}
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php
new file mode 100644
index 0000000..a819e05
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class ArithmeticExpression extends Node
15{
16 public Node|string|null $simpleArithmeticExpression = null;
17
18 public Subselect|null $subselect = null;
19
20 public function isSimpleArithmeticExpression(): bool
21 {
22 return (bool) $this->simpleArithmeticExpression;
23 }
24
25 public function isSubselect(): bool
26 {
27 return (bool) $this->subselect;
28 }
29
30 public function dispatch(SqlWalker $walker): string
31 {
32 return $walker->walkArithmeticExpression($this);
33 }
34}
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php
new file mode 100644
index 0000000..278a921
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php
@@ -0,0 +1,36 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
11 *
12 * @link www.doctrine-project.org
13 */
14class ArithmeticFactor extends Node
15{
16 public function __construct(
17 public mixed $arithmeticPrimary,
18 public bool|null $sign = null,
19 ) {
20 }
21
22 public function isPositiveSigned(): bool
23 {
24 return $this->sign === true;
25 }
26
27 public function isNegativeSigned(): bool
28 {
29 return $this->sign === false;
30 }
31
32 public function dispatch(SqlWalker $walker): string
33 {
34 return $walker->walkArithmeticFactor($this);
35 }
36}
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php
new file mode 100644
index 0000000..b233612
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
11 *
12 * @link www.doctrine-project.org
13 */
14class ArithmeticTerm extends Node
15{
16 /** @param mixed[] $arithmeticFactors */
17 public function __construct(public array $arithmeticFactors)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkArithmeticTerm($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php
new file mode 100644
index 0000000..c13292b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class BetweenExpression extends Node
10{
11 public function __construct(
12 public ArithmeticExpression $expression,
13 public ArithmeticExpression $leftBetweenExpression,
14 public ArithmeticExpression $rightBetweenExpression,
15 public bool $not = false,
16 ) {
17 }
18
19 public function dispatch(SqlWalker $walker): string
20 {
21 return $walker->walkBetweenExpression($this);
22 }
23}
diff --git a/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php
new file mode 100644
index 0000000..89f025f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class CoalesceExpression extends Node
15{
16 /** @param mixed[] $scalarExpressions */
17 public function __construct(public array $scalarExpressions)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkCoalesceExpression($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php
new file mode 100644
index 0000000..a62a191
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class CollectionMemberExpression extends Node
15{
16 public function __construct(
17 public mixed $entityExpression,
18 public PathExpression $collectionValuedPathExpression,
19 public bool $not = false,
20 ) {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkCollectionMemberExpression($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php
new file mode 100644
index 0000000..a7d91f9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) |
11 * StringExpression ComparisonOperator (StringExpression | QuantifiedExpression) |
12 * BooleanExpression ("=" | "<>" | "!=") (BooleanExpression | QuantifiedExpression) |
13 * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) |
14 * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) |
15 * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression)
16 *
17 * @link www.doctrine-project.org
18 */
19class ComparisonExpression extends Node
20{
21 public function __construct(
22 public Node|string $leftExpression,
23 public string $operator,
24 public Node|string $rightExpression,
25 ) {
26 }
27
28 public function dispatch(SqlWalker $walker): string
29 {
30 return $walker->walkComparisonExpression($this);
31 }
32}
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php
new file mode 100644
index 0000000..26a98e5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
11 *
12 * @link www.doctrine-project.org
13 */
14class ConditionalExpression extends Node
15{
16 /** @param mixed[] $conditionalTerms */
17 public function __construct(public array $conditionalTerms)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkConditionalExpression($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php
new file mode 100644
index 0000000..7881743
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ConditionalFactor ::= ["NOT"] ConditionalPrimary
11 *
12 * @link www.doctrine-project.org
13 */
14class ConditionalFactor extends Node implements Phase2OptimizableConditional
15{
16 public function __construct(
17 public ConditionalPrimary $conditionalPrimary,
18 public bool $not = false,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkConditionalFactor($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php
new file mode 100644
index 0000000..9344cd9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class ConditionalPrimary extends Node implements Phase2OptimizableConditional
15{
16 public Node|null $simpleConditionalExpression = null;
17
18 public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null;
19
20 public function isSimpleConditionalExpression(): bool
21 {
22 return (bool) $this->simpleConditionalExpression;
23 }
24
25 public function isConditionalExpression(): bool
26 {
27 return (bool) $this->conditionalExpression;
28 }
29
30 public function dispatch(SqlWalker $walker): string
31 {
32 return $walker->walkConditionalPrimary($this);
33 }
34}
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php
new file mode 100644
index 0000000..dcea50b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
11 *
12 * @link www.doctrine-project.org
13 */
14class ConditionalTerm extends Node implements Phase2OptimizableConditional
15{
16 /** @param mixed[] $conditionalFactors */
17 public function __construct(public array $conditionalFactors)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkConditionalTerm($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteClause.php b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php
new file mode 100644
index 0000000..25e9085
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName [["AS"] AliasIdentificationVariable]
11 *
12 * @link www.doctrine-project.org
13 */
14class DeleteClause extends Node
15{
16 public string $aliasIdentificationVariable;
17
18 public function __construct(public string $abstractSchemaName)
19 {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkDeleteClause($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php
new file mode 100644
index 0000000..f367d09
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * DeleteStatement = DeleteClause [WhereClause]
11 *
12 * @link www.doctrine-project.org
13 */
14class DeleteStatement extends Node
15{
16 public WhereClause|null $whereClause = null;
17
18 public function __construct(public DeleteClause $deleteClause)
19 {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkDeleteStatement($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php
new file mode 100644
index 0000000..9978800
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
11 *
12 * @link www.doctrine-project.org
13 */
14class EmptyCollectionComparisonExpression extends Node
15{
16 public function __construct(
17 public PathExpression $expression,
18 public bool $not = false,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkEmptyCollectionComparisonExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php
new file mode 100644
index 0000000..72757f4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class ExistsExpression extends Node
15{
16 public function __construct(
17 public Subselect $subselect,
18 public bool $not = false,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkExistsExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/FromClause.php b/vendor/doctrine/orm/src/Query/AST/FromClause.php
new file mode 100644
index 0000000..0b74393
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/FromClause.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}
11 *
12 * @link www.doctrine-project.org
13 */
14class FromClause extends Node
15{
16 /** @param mixed[] $identificationVariableDeclarations */
17 public function __construct(public array $identificationVariableDeclarations)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkFromClause($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php
new file mode 100644
index 0000000..4edff06
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php
@@ -0,0 +1,37 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "ABS" "(" SimpleArithmeticExpression ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class AbsFunction extends FunctionNode
18{
19 public Node|string $simpleArithmeticExpression;
20
21 public function getSql(SqlWalker $sqlWalker): string
22 {
23 return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression(
24 $this->simpleArithmeticExpression,
25 ) . ')';
26 }
27
28 public function parse(Parser $parser): void
29 {
30 $parser->match(TokenType::T_IDENTIFIER);
31 $parser->match(TokenType::T_OPEN_PARENTHESIS);
32
33 $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression();
34
35 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
36 }
37}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php
new file mode 100644
index 0000000..ba7b7f3
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10
11/**
12 * "AVG" "(" ["DISTINCT"] StringPrimary ")"
13 */
14final class AvgFunction extends FunctionNode
15{
16 private AggregateExpression $aggregateExpression;
17
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $this->aggregateExpression->dispatch($sqlWalker);
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $this->aggregateExpression = $parser->AggregateExpression();
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php
new file mode 100644
index 0000000..f2d3146
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php
@@ -0,0 +1,43 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class BitAndFunction extends FunctionNode
18{
19 public Node $firstArithmetic;
20 public Node $secondArithmetic;
21
22 public function getSql(SqlWalker $sqlWalker): string
23 {
24 $platform = $sqlWalker->getConnection()->getDatabasePlatform();
25
26 return $platform->getBitAndComparisonExpression(
27 $this->firstArithmetic->dispatch($sqlWalker),
28 $this->secondArithmetic->dispatch($sqlWalker),
29 );
30 }
31
32 public function parse(Parser $parser): void
33 {
34 $parser->match(TokenType::T_IDENTIFIER);
35 $parser->match(TokenType::T_OPEN_PARENTHESIS);
36
37 $this->firstArithmetic = $parser->ArithmeticPrimary();
38 $parser->match(TokenType::T_COMMA);
39 $this->secondArithmetic = $parser->ArithmeticPrimary();
40
41 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
42 }
43}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php
new file mode 100644
index 0000000..f3f84da
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php
@@ -0,0 +1,43 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class BitOrFunction extends FunctionNode
18{
19 public Node $firstArithmetic;
20 public Node $secondArithmetic;
21
22 public function getSql(SqlWalker $sqlWalker): string
23 {
24 $platform = $sqlWalker->getConnection()->getDatabasePlatform();
25
26 return $platform->getBitOrComparisonExpression(
27 $this->firstArithmetic->dispatch($sqlWalker),
28 $this->secondArithmetic->dispatch($sqlWalker),
29 );
30 }
31
32 public function parse(Parser $parser): void
33 {
34 $parser->match(TokenType::T_IDENTIFIER);
35 $parser->match(TokenType::T_OPEN_PARENTHESIS);
36
37 $this->firstArithmetic = $parser->ArithmeticPrimary();
38 $parser->match(TokenType::T_COMMA);
39 $this->secondArithmetic = $parser->ArithmeticPrimary();
40
41 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
42 }
43}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php
new file mode 100644
index 0000000..5b8d696
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php
@@ -0,0 +1,58 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary }* ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class ConcatFunction extends FunctionNode
18{
19 public Node $firstStringPrimary;
20 public Node $secondStringPrimary;
21
22 /** @psalm-var list<Node> */
23 public array $concatExpressions = [];
24
25 public function getSql(SqlWalker $sqlWalker): string
26 {
27 $platform = $sqlWalker->getConnection()->getDatabasePlatform();
28
29 $args = [];
30
31 foreach ($this->concatExpressions as $expression) {
32 $args[] = $sqlWalker->walkStringPrimary($expression);
33 }
34
35 return $platform->getConcatExpression(...$args);
36 }
37
38 public function parse(Parser $parser): void
39 {
40 $parser->match(TokenType::T_IDENTIFIER);
41 $parser->match(TokenType::T_OPEN_PARENTHESIS);
42
43 $this->firstStringPrimary = $parser->StringPrimary();
44 $this->concatExpressions[] = $this->firstStringPrimary;
45
46 $parser->match(TokenType::T_COMMA);
47
48 $this->secondStringPrimary = $parser->StringPrimary();
49 $this->concatExpressions[] = $this->secondStringPrimary;
50
51 while ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) {
52 $parser->match(TokenType::T_COMMA);
53 $this->concatExpressions[] = $parser->StringPrimary();
54 }
55
56 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
57 }
58}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php
new file mode 100644
index 0000000..dc926a5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\DBAL\Types\Types;
9use Doctrine\ORM\Query\AST\AggregateExpression;
10use Doctrine\ORM\Query\AST\TypedExpression;
11use Doctrine\ORM\Query\Parser;
12use Doctrine\ORM\Query\SqlWalker;
13
14/**
15 * "COUNT" "(" ["DISTINCT"] StringPrimary ")"
16 */
17final class CountFunction extends FunctionNode implements TypedExpression
18{
19 private AggregateExpression $aggregateExpression;
20
21 public function getSql(SqlWalker $sqlWalker): string
22 {
23 return $this->aggregateExpression->dispatch($sqlWalker);
24 }
25
26 public function parse(Parser $parser): void
27 {
28 $this->aggregateExpression = $parser->AggregateExpression();
29 }
30
31 public function getReturnType(): Type
32 {
33 return Type::getType(Types::INTEGER);
34 }
35}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php
new file mode 100644
index 0000000..cec9632
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\Parser;
8use Doctrine\ORM\Query\SqlWalker;
9use Doctrine\ORM\Query\TokenType;
10
11/**
12 * "CURRENT_DATE"
13 *
14 * @link www.doctrine-project.org
15 */
16class CurrentDateFunction extends FunctionNode
17{
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentDateSQL();
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $parser->match(TokenType::T_IDENTIFIER);
26 $parser->match(TokenType::T_OPEN_PARENTHESIS);
27 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php
new file mode 100644
index 0000000..6473fce
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\Parser;
8use Doctrine\ORM\Query\SqlWalker;
9use Doctrine\ORM\Query\TokenType;
10
11/**
12 * "CURRENT_TIME"
13 *
14 * @link www.doctrine-project.org
15 */
16class CurrentTimeFunction extends FunctionNode
17{
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimeSQL();
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $parser->match(TokenType::T_IDENTIFIER);
26 $parser->match(TokenType::T_OPEN_PARENTHESIS);
27 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php
new file mode 100644
index 0000000..edcd27c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\Parser;
8use Doctrine\ORM\Query\SqlWalker;
9use Doctrine\ORM\Query\TokenType;
10
11/**
12 * "CURRENT_TIMESTAMP"
13 *
14 * @link www.doctrine-project.org
15 */
16class CurrentTimestampFunction extends FunctionNode
17{
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimestampSQL();
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $parser->match(TokenType::T_IDENTIFIER);
26 $parser->match(TokenType::T_OPEN_PARENTHESIS);
27 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php
new file mode 100644
index 0000000..12920dc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php
@@ -0,0 +1,83 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\ASTException;
8use Doctrine\ORM\Query\AST\Node;
9use Doctrine\ORM\Query\Parser;
10use Doctrine\ORM\Query\QueryException;
11use Doctrine\ORM\Query\SqlWalker;
12use Doctrine\ORM\Query\TokenType;
13
14use function strtolower;
15
16/**
17 * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
18 *
19 * @link www.doctrine-project.org
20 */
21class DateAddFunction extends FunctionNode
22{
23 public Node $firstDateExpression;
24 public Node $intervalExpression;
25 public Node $unit;
26
27 public function getSql(SqlWalker $sqlWalker): string
28 {
29 return match (strtolower((string) $this->unit->value)) {
30 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddSecondsExpression(
31 $this->firstDateExpression->dispatch($sqlWalker),
32 $this->dispatchIntervalExpression($sqlWalker),
33 ),
34 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMinutesExpression(
35 $this->firstDateExpression->dispatch($sqlWalker),
36 $this->dispatchIntervalExpression($sqlWalker),
37 ),
38 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddHourExpression(
39 $this->firstDateExpression->dispatch($sqlWalker),
40 $this->dispatchIntervalExpression($sqlWalker),
41 ),
42 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression(
43 $this->firstDateExpression->dispatch($sqlWalker),
44 $this->dispatchIntervalExpression($sqlWalker),
45 ),
46 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddWeeksExpression(
47 $this->firstDateExpression->dispatch($sqlWalker),
48 $this->dispatchIntervalExpression($sqlWalker),
49 ),
50 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression(
51 $this->firstDateExpression->dispatch($sqlWalker),
52 $this->dispatchIntervalExpression($sqlWalker),
53 ),
54 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddYearsExpression(
55 $this->firstDateExpression->dispatch($sqlWalker),
56 $this->dispatchIntervalExpression($sqlWalker),
57 ),
58 default => throw QueryException::semanticalError(
59 'DATE_ADD() only supports units of type second, minute, hour, day, week, month and year.',
60 ),
61 };
62 }
63
64 /** @throws ASTException */
65 private function dispatchIntervalExpression(SqlWalker $sqlWalker): string
66 {
67 return $this->intervalExpression->dispatch($sqlWalker);
68 }
69
70 public function parse(Parser $parser): void
71 {
72 $parser->match(TokenType::T_IDENTIFIER);
73 $parser->match(TokenType::T_OPEN_PARENTHESIS);
74
75 $this->firstDateExpression = $parser->ArithmeticPrimary();
76 $parser->match(TokenType::T_COMMA);
77 $this->intervalExpression = $parser->ArithmeticPrimary();
78 $parser->match(TokenType::T_COMMA);
79 $this->unit = $parser->StringPrimary();
80
81 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
82 }
83}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php
new file mode 100644
index 0000000..55598c0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php
@@ -0,0 +1,41 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class DateDiffFunction extends FunctionNode
18{
19 public Node $date1;
20 public Node $date2;
21
22 public function getSql(SqlWalker $sqlWalker): string
23 {
24 return $sqlWalker->getConnection()->getDatabasePlatform()->getDateDiffExpression(
25 $this->date1->dispatch($sqlWalker),
26 $this->date2->dispatch($sqlWalker),
27 );
28 }
29
30 public function parse(Parser $parser): void
31 {
32 $parser->match(TokenType::T_IDENTIFIER);
33 $parser->match(TokenType::T_OPEN_PARENTHESIS);
34
35 $this->date1 = $parser->ArithmeticPrimary();
36 $parser->match(TokenType::T_COMMA);
37 $this->date2 = $parser->ArithmeticPrimary();
38
39 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
40 }
41}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php
new file mode 100644
index 0000000..5363680
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php
@@ -0,0 +1,62 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\ASTException;
8use Doctrine\ORM\Query\QueryException;
9use Doctrine\ORM\Query\SqlWalker;
10
11use function strtolower;
12
13/**
14 * "DATE_SUB(date1, interval, unit)"
15 *
16 * @link www.doctrine-project.org
17 */
18class DateSubFunction extends DateAddFunction
19{
20 public function getSql(SqlWalker $sqlWalker): string
21 {
22 return match (strtolower((string) $this->unit->value)) {
23 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubSecondsExpression(
24 $this->firstDateExpression->dispatch($sqlWalker),
25 $this->dispatchIntervalExpression($sqlWalker),
26 ),
27 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMinutesExpression(
28 $this->firstDateExpression->dispatch($sqlWalker),
29 $this->dispatchIntervalExpression($sqlWalker),
30 ),
31 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubHourExpression(
32 $this->firstDateExpression->dispatch($sqlWalker),
33 $this->dispatchIntervalExpression($sqlWalker),
34 ),
35 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression(
36 $this->firstDateExpression->dispatch($sqlWalker),
37 $this->dispatchIntervalExpression($sqlWalker),
38 ),
39 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubWeeksExpression(
40 $this->firstDateExpression->dispatch($sqlWalker),
41 $this->dispatchIntervalExpression($sqlWalker),
42 ),
43 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression(
44 $this->firstDateExpression->dispatch($sqlWalker),
45 $this->dispatchIntervalExpression($sqlWalker),
46 ),
47 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubYearsExpression(
48 $this->firstDateExpression->dispatch($sqlWalker),
49 $this->dispatchIntervalExpression($sqlWalker),
50 ),
51 default => throw QueryException::semanticalError(
52 'DATE_SUB() only supports units of type second, minute, hour, day, week, month and year.',
53 ),
54 };
55 }
56
57 /** @throws ASTException */
58 private function dispatchIntervalExpression(SqlWalker $sqlWalker): string
59 {
60 return $this->intervalExpression->dispatch($sqlWalker);
61 }
62}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php
new file mode 100644
index 0000000..4cc549e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10
11/**
12 * Abstract Function Node.
13 *
14 * @link www.doctrine-project.org
15 *
16 * @psalm-consistent-constructor
17 */
18abstract class FunctionNode extends Node
19{
20 public function __construct(public string $name)
21 {
22 }
23
24 abstract public function getSql(SqlWalker $sqlWalker): string;
25
26 public function dispatch(SqlWalker $sqlWalker): string
27 {
28 return $sqlWalker->walkFunction($this);
29 }
30
31 abstract public function parse(Parser $parser): void;
32}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php
new file mode 100644
index 0000000..1dd1bf5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php
@@ -0,0 +1,90 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\PathExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\QueryException;
10use Doctrine\ORM\Query\SqlWalker;
11use Doctrine\ORM\Query\TokenType;
12
13use function assert;
14use function reset;
15use function sprintf;
16
17/**
18 * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
19 *
20 * @link www.doctrine-project.org
21 */
22class IdentityFunction extends FunctionNode
23{
24 public PathExpression $pathExpression;
25
26 public string|null $fieldMapping = null;
27
28 public function getSql(SqlWalker $sqlWalker): string
29 {
30 assert($this->pathExpression->field !== null);
31 $entityManager = $sqlWalker->getEntityManager();
32 $platform = $entityManager->getConnection()->getDatabasePlatform();
33 $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy();
34 $dqlAlias = $this->pathExpression->identificationVariable;
35 $assocField = $this->pathExpression->field;
36 $assoc = $sqlWalker->getMetadataForDqlAlias($dqlAlias)->associationMappings[$assocField];
37 $targetEntity = $entityManager->getClassMetadata($assoc->targetEntity);
38
39 assert($assoc->isToOneOwningSide());
40 $joinColumn = reset($assoc->joinColumns);
41
42 if ($this->fieldMapping !== null) {
43 if (! isset($targetEntity->fieldMappings[$this->fieldMapping])) {
44 throw new QueryException(sprintf('Undefined reference field mapping "%s"', $this->fieldMapping));
45 }
46
47 $field = $targetEntity->fieldMappings[$this->fieldMapping];
48 $joinColumn = null;
49
50 foreach ($assoc->joinColumns as $mapping) {
51 if ($mapping->referencedColumnName === $field->columnName) {
52 $joinColumn = $mapping;
53
54 break;
55 }
56 }
57
58 if ($joinColumn === null) {
59 throw new QueryException(sprintf('Unable to resolve the reference field mapping "%s"', $this->fieldMapping));
60 }
61 }
62
63 // The table with the relation may be a subclass, so get the table name from the association definition
64 $tableName = $entityManager->getClassMetadata($assoc->sourceEntity)->getTableName();
65
66 $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias);
67 $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform);
68
69 return $tableAlias . '.' . $columnName;
70 }
71
72 public function parse(Parser $parser): void
73 {
74 $parser->match(TokenType::T_IDENTIFIER);
75 $parser->match(TokenType::T_OPEN_PARENTHESIS);
76
77 $this->pathExpression = $parser->SingleValuedAssociationPathExpression();
78
79 if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) {
80 $parser->match(TokenType::T_COMMA);
81 $parser->match(TokenType::T_STRING);
82
83 $token = $parser->getLexer()->token;
84 assert($token !== null);
85 $this->fieldMapping = $token->value;
86 }
87
88 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
89 }
90}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php
new file mode 100644
index 0000000..3994918
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php
@@ -0,0 +1,45 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\DBAL\Types\Types;
9use Doctrine\ORM\Query\AST\Node;
10use Doctrine\ORM\Query\AST\TypedExpression;
11use Doctrine\ORM\Query\Parser;
12use Doctrine\ORM\Query\SqlWalker;
13use Doctrine\ORM\Query\TokenType;
14
15/**
16 * "LENGTH" "(" StringPrimary ")"
17 *
18 * @link www.doctrine-project.org
19 */
20class LengthFunction extends FunctionNode implements TypedExpression
21{
22 public Node $stringPrimary;
23
24 public function getSql(SqlWalker $sqlWalker): string
25 {
26 return $sqlWalker->getConnection()->getDatabasePlatform()->getLengthExpression(
27 $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary),
28 );
29 }
30
31 public function parse(Parser $parser): void
32 {
33 $parser->match(TokenType::T_IDENTIFIER);
34 $parser->match(TokenType::T_OPEN_PARENTHESIS);
35
36 $this->stringPrimary = $parser->StringPrimary();
37
38 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
39 }
40
41 public function getReturnType(): Type
42 {
43 return Type::getType(Types::INTEGER);
44 }
45}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php
new file mode 100644
index 0000000..c0d3b4a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php
@@ -0,0 +1,62 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")"
14 *
15 * @link www.doctrine-project.org
16 */
17class LocateFunction extends FunctionNode
18{
19 public Node|string $firstStringPrimary;
20 public Node|string $secondStringPrimary;
21
22 public Node|string|bool $simpleArithmeticExpression = false;
23
24 public function getSql(SqlWalker $sqlWalker): string
25 {
26 $platform = $sqlWalker->getConnection()->getDatabasePlatform();
27
28 $firstString = $sqlWalker->walkStringPrimary($this->firstStringPrimary);
29 $secondString = $sqlWalker->walkStringPrimary($this->secondStringPrimary);
30
31 if ($this->simpleArithmeticExpression) {
32 return $platform->getLocateExpression(
33 $secondString,
34 $firstString,
35 $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression),
36 );
37 }
38
39 return $platform->getLocateExpression($secondString, $firstString);
40 }
41
42 public function parse(Parser $parser): void
43 {
44 $parser->match(TokenType::T_IDENTIFIER);
45 $parser->match(TokenType::T_OPEN_PARENTHESIS);
46
47 $this->firstStringPrimary = $parser->StringPrimary();
48
49 $parser->match(TokenType::T_COMMA);
50
51 $this->secondStringPrimary = $parser->StringPrimary();
52
53 $lexer = $parser->getLexer();
54 if ($lexer->isNextToken(TokenType::T_COMMA)) {
55 $parser->match(TokenType::T_COMMA);
56
57 $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression();
58 }
59
60 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
61 }
62}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php
new file mode 100644
index 0000000..8ae337a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12use function sprintf;
13
14/**
15 * "LOWER" "(" StringPrimary ")"
16 *
17 * @link www.doctrine-project.org
18 */
19class LowerFunction extends FunctionNode
20{
21 public Node $stringPrimary;
22
23 public function getSql(SqlWalker $sqlWalker): string
24 {
25 return sprintf(
26 'LOWER(%s)',
27 $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary),
28 );
29 }
30
31 public function parse(Parser $parser): void
32 {
33 $parser->match(TokenType::T_IDENTIFIER);
34 $parser->match(TokenType::T_OPEN_PARENTHESIS);
35
36 $this->stringPrimary = $parser->StringPrimary();
37
38 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
39 }
40}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php
new file mode 100644
index 0000000..8a6eecf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10
11/**
12 * "MAX" "(" ["DISTINCT"] StringPrimary ")"
13 */
14final class MaxFunction extends FunctionNode
15{
16 private AggregateExpression $aggregateExpression;
17
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $this->aggregateExpression->dispatch($sqlWalker);
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $this->aggregateExpression = $parser->AggregateExpression();
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php
new file mode 100644
index 0000000..98d73a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10
11/**
12 * "MIN" "(" ["DISTINCT"] StringPrimary ")"
13 */
14final class MinFunction extends FunctionNode
15{
16 private AggregateExpression $aggregateExpression;
17
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $this->aggregateExpression->dispatch($sqlWalker);
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $this->aggregateExpression = $parser->AggregateExpression();
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php
new file mode 100644
index 0000000..7c1af0b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php
@@ -0,0 +1,43 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class ModFunction extends FunctionNode
18{
19 public Node|string $firstSimpleArithmeticExpression;
20 public Node|string $secondSimpleArithmeticExpression;
21
22 public function getSql(SqlWalker $sqlWalker): string
23 {
24 return $sqlWalker->getConnection()->getDatabasePlatform()->getModExpression(
25 $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression),
26 $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression),
27 );
28 }
29
30 public function parse(Parser $parser): void
31 {
32 $parser->match(TokenType::T_IDENTIFIER);
33 $parser->match(TokenType::T_OPEN_PARENTHESIS);
34
35 $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression();
36
37 $parser->match(TokenType::T_COMMA);
38
39 $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression();
40
41 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
42 }
43}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php
new file mode 100644
index 0000000..87ee713
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php
@@ -0,0 +1,113 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\PathExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12use function assert;
13
14/**
15 * "SIZE" "(" CollectionValuedPathExpression ")"
16 *
17 * @link www.doctrine-project.org
18 */
19class SizeFunction extends FunctionNode
20{
21 public PathExpression $collectionPathExpression;
22
23 /**
24 * @inheritdoc
25 * @todo If the collection being counted is already joined, the SQL can be simpler (more efficient).
26 */
27 public function getSql(SqlWalker $sqlWalker): string
28 {
29 assert($this->collectionPathExpression->field !== null);
30 $entityManager = $sqlWalker->getEntityManager();
31 $platform = $entityManager->getConnection()->getDatabasePlatform();
32 $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy();
33 $dqlAlias = $this->collectionPathExpression->identificationVariable;
34 $assocField = $this->collectionPathExpression->field;
35
36 $class = $sqlWalker->getMetadataForDqlAlias($dqlAlias);
37 $assoc = $class->associationMappings[$assocField];
38 $sql = 'SELECT COUNT(*) FROM ';
39
40 if ($assoc->isOneToMany()) {
41 $targetClass = $entityManager->getClassMetadata($assoc->targetEntity);
42 $targetTableAlias = $sqlWalker->getSQLTableAlias($targetClass->getTableName());
43 $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias);
44
45 $sql .= $quoteStrategy->getTableName($targetClass, $platform) . ' ' . $targetTableAlias . ' WHERE ';
46
47 $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy];
48 assert($owningAssoc->isManyToOne());
49
50 $first = true;
51
52 foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) {
53 if ($first) {
54 $first = false;
55 } else {
56 $sql .= ' AND ';
57 }
58
59 $sql .= $targetTableAlias . '.' . $sourceColumn
60 . ' = '
61 . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform);
62 }
63 } else { // many-to-many
64 assert($assoc->isManyToMany());
65 $owningAssoc = $entityManager->getMetadataFactory()->getOwningSide($assoc);
66 $joinTable = $owningAssoc->joinTable;
67
68 // SQL table aliases
69 $joinTableAlias = $sqlWalker->getSQLTableAlias($joinTable->name);
70 $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias);
71
72 // join to target table
73 $targetClass = $entityManager->getClassMetadata($assoc->targetEntity);
74 $sql .= $quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $platform) . ' ' . $joinTableAlias . ' WHERE ';
75
76 $joinColumns = $assoc->isOwningSide()
77 ? $joinTable->joinColumns
78 : $joinTable->inverseJoinColumns;
79
80 $first = true;
81
82 foreach ($joinColumns as $joinColumn) {
83 if ($first) {
84 $first = false;
85 } else {
86 $sql .= ' AND ';
87 }
88
89 $sourceColumnName = $quoteStrategy->getColumnName(
90 $class->fieldNames[$joinColumn->referencedColumnName],
91 $class,
92 $platform,
93 );
94
95 $sql .= $joinTableAlias . '.' . $joinColumn->name
96 . ' = '
97 . $sourceTableAlias . '.' . $sourceColumnName;
98 }
99 }
100
101 return '(' . $sql . ')';
102 }
103
104 public function parse(Parser $parser): void
105 {
106 $parser->match(TokenType::T_IDENTIFIER);
107 $parser->match(TokenType::T_OPEN_PARENTHESIS);
108
109 $this->collectionPathExpression = $parser->CollectionValuedPathExpression();
110
111 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
112 }
113}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php
new file mode 100644
index 0000000..e643663
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12use function sprintf;
13
14/**
15 * "SQRT" "(" SimpleArithmeticExpression ")"
16 *
17 * @link www.doctrine-project.org
18 */
19class SqrtFunction extends FunctionNode
20{
21 public Node|string $simpleArithmeticExpression;
22
23 public function getSql(SqlWalker $sqlWalker): string
24 {
25 return sprintf(
26 'SQRT(%s)',
27 $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression),
28 );
29 }
30
31 public function parse(Parser $parser): void
32 {
33 $parser->match(TokenType::T_IDENTIFIER);
34 $parser->match(TokenType::T_OPEN_PARENTHESIS);
35
36 $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression();
37
38 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
39 }
40}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php
new file mode 100644
index 0000000..5744f08
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php
@@ -0,0 +1,58 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12/**
13 * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")"
14 *
15 * @link www.doctrine-project.org
16 */
17class SubstringFunction extends FunctionNode
18{
19 public Node $stringPrimary;
20
21 public Node|string $firstSimpleArithmeticExpression;
22 public Node|string|null $secondSimpleArithmeticExpression = null;
23
24 public function getSql(SqlWalker $sqlWalker): string
25 {
26 $optionalSecondSimpleArithmeticExpression = null;
27 if ($this->secondSimpleArithmeticExpression !== null) {
28 $optionalSecondSimpleArithmeticExpression = $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression);
29 }
30
31 return $sqlWalker->getConnection()->getDatabasePlatform()->getSubstringExpression(
32 $sqlWalker->walkStringPrimary($this->stringPrimary),
33 $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression),
34 $optionalSecondSimpleArithmeticExpression,
35 );
36 }
37
38 public function parse(Parser $parser): void
39 {
40 $parser->match(TokenType::T_IDENTIFIER);
41 $parser->match(TokenType::T_OPEN_PARENTHESIS);
42
43 $this->stringPrimary = $parser->StringPrimary();
44
45 $parser->match(TokenType::T_COMMA);
46
47 $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression();
48
49 $lexer = $parser->getLexer();
50 if ($lexer->isNextToken(TokenType::T_COMMA)) {
51 $parser->match(TokenType::T_COMMA);
52
53 $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression();
54 }
55
56 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
57 }
58}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php
new file mode 100644
index 0000000..588dce9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10
11/**
12 * "SUM" "(" ["DISTINCT"] StringPrimary ")"
13 */
14final class SumFunction extends FunctionNode
15{
16 private AggregateExpression $aggregateExpression;
17
18 public function getSql(SqlWalker $sqlWalker): string
19 {
20 return $this->aggregateExpression->dispatch($sqlWalker);
21 }
22
23 public function parse(Parser $parser): void
24 {
25 $this->aggregateExpression = $parser->AggregateExpression();
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php
new file mode 100644
index 0000000..e0a3e99
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php
@@ -0,0 +1,119 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\DBAL\Platforms\TrimMode;
8use Doctrine\ORM\Query\AST\Node;
9use Doctrine\ORM\Query\Parser;
10use Doctrine\ORM\Query\SqlWalker;
11use Doctrine\ORM\Query\TokenType;
12
13use function assert;
14use function strcasecmp;
15
16/**
17 * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")"
18 *
19 * @link www.doctrine-project.org
20 */
21class TrimFunction extends FunctionNode
22{
23 public bool $leading = false;
24 public bool $trailing = false;
25 public bool $both = false;
26 public string|false $trimChar = false;
27 public Node $stringPrimary;
28
29 public function getSql(SqlWalker $sqlWalker): string
30 {
31 $stringPrimary = $sqlWalker->walkStringPrimary($this->stringPrimary);
32 $platform = $sqlWalker->getConnection()->getDatabasePlatform();
33 $trimMode = $this->getTrimMode();
34
35 if ($this->trimChar !== false) {
36 return $platform->getTrimExpression(
37 $stringPrimary,
38 $trimMode,
39 $platform->quoteStringLiteral($this->trimChar),
40 );
41 }
42
43 return $platform->getTrimExpression($stringPrimary, $trimMode);
44 }
45
46 public function parse(Parser $parser): void
47 {
48 $lexer = $parser->getLexer();
49
50 $parser->match(TokenType::T_IDENTIFIER);
51 $parser->match(TokenType::T_OPEN_PARENTHESIS);
52
53 $this->parseTrimMode($parser);
54
55 if ($lexer->isNextToken(TokenType::T_STRING)) {
56 $parser->match(TokenType::T_STRING);
57
58 assert($lexer->token !== null);
59 $this->trimChar = $lexer->token->value;
60 }
61
62 if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) {
63 $parser->match(TokenType::T_FROM);
64 }
65
66 $this->stringPrimary = $parser->StringPrimary();
67
68 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
69 }
70
71 /** @psalm-return TrimMode::* */
72 private function getTrimMode(): TrimMode|int
73 {
74 if ($this->leading) {
75 return TrimMode::LEADING;
76 }
77
78 if ($this->trailing) {
79 return TrimMode::TRAILING;
80 }
81
82 if ($this->both) {
83 return TrimMode::BOTH;
84 }
85
86 return TrimMode::UNSPECIFIED;
87 }
88
89 private function parseTrimMode(Parser $parser): void
90 {
91 $lexer = $parser->getLexer();
92 assert($lexer->lookahead !== null);
93 $value = $lexer->lookahead->value;
94
95 if (strcasecmp('leading', $value) === 0) {
96 $parser->match(TokenType::T_LEADING);
97
98 $this->leading = true;
99
100 return;
101 }
102
103 if (strcasecmp('trailing', $value) === 0) {
104 $parser->match(TokenType::T_TRAILING);
105
106 $this->trailing = true;
107
108 return;
109 }
110
111 if (strcasecmp('both', $value) === 0) {
112 $parser->match(TokenType::T_BOTH);
113
114 $this->both = true;
115
116 return;
117 }
118 }
119}
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php
new file mode 100644
index 0000000..1ecef66
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST\Functions;
6
7use Doctrine\ORM\Query\AST\Node;
8use Doctrine\ORM\Query\Parser;
9use Doctrine\ORM\Query\SqlWalker;
10use Doctrine\ORM\Query\TokenType;
11
12use function sprintf;
13
14/**
15 * "UPPER" "(" StringPrimary ")"
16 *
17 * @link www.doctrine-project.org
18 */
19class UpperFunction extends FunctionNode
20{
21 public Node $stringPrimary;
22
23 public function getSql(SqlWalker $sqlWalker): string
24 {
25 return sprintf(
26 'UPPER(%s)',
27 $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary),
28 );
29 }
30
31 public function parse(Parser $parser): void
32 {
33 $parser->match(TokenType::T_IDENTIFIER);
34 $parser->match(TokenType::T_OPEN_PARENTHESIS);
35
36 $this->stringPrimary = $parser->StringPrimary();
37
38 $parser->match(TokenType::T_CLOSE_PARENTHESIS);
39 }
40}
diff --git a/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php
new file mode 100644
index 0000000..39d760a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
11 *
12 * @link www.doctrine-project.org
13 */
14class GeneralCaseExpression extends Node
15{
16 /** @param mixed[] $whenClauses */
17 public function __construct(
18 public array $whenClauses,
19 public mixed $elseScalarExpression = null,
20 ) {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkGeneralCaseExpression($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/GroupByClause.php b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php
new file mode 100644
index 0000000..eb0f1b9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class GroupByClause extends Node
10{
11 /** @param mixed[] $groupByItems */
12 public function __construct(public array $groupByItems)
13 {
14 }
15
16 public function dispatch(SqlWalker $walker): string
17 {
18 return $walker->walkGroupByClause($this);
19 }
20}
diff --git a/vendor/doctrine/orm/src/Query/AST/HavingClause.php b/vendor/doctrine/orm/src/Query/AST/HavingClause.php
new file mode 100644
index 0000000..0d4d821
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/HavingClause.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class HavingClause extends Node
10{
11 public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression)
12 {
13 }
14
15 public function dispatch(SqlWalker $walker): string
16 {
17 return $walker->walkHavingClause($this);
18 }
19}
diff --git a/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php
new file mode 100644
index 0000000..c4c7cca
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}*
11 *
12 * @link www.doctrine-project.org
13 */
14class IdentificationVariableDeclaration extends Node
15{
16 /** @param mixed[] $joins */
17 public function __construct(
18 public RangeVariableDeclaration|null $rangeVariableDeclaration = null,
19 public IndexBy|null $indexBy = null,
20 public array $joins = [],
21 ) {
22 }
23
24 public function dispatch(SqlWalker $walker): string
25 {
26 return $walker->walkIdentificationVariableDeclaration($this);
27 }
28}
diff --git a/vendor/doctrine/orm/src/Query/AST/InListExpression.php b/vendor/doctrine/orm/src/Query/AST/InListExpression.php
new file mode 100644
index 0000000..dc0f32b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/InListExpression.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class InListExpression extends Node
10{
11 /** @param non-empty-list<mixed> $literals */
12 public function __construct(
13 public ArithmeticExpression $expression,
14 public array $literals,
15 public bool $not = false,
16 ) {
17 }
18
19 public function dispatch(SqlWalker $walker): string
20 {
21 return $walker->walkInListExpression($this);
22 }
23}
diff --git a/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php
new file mode 100644
index 0000000..1128285
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php
@@ -0,0 +1,22 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class InSubselectExpression extends Node
10{
11 public function __construct(
12 public ArithmeticExpression $expression,
13 public Subselect $subselect,
14 public bool $not = false,
15 ) {
16 }
17
18 public function dispatch(SqlWalker $walker): string
19 {
20 return $walker->walkInSubselectExpression($this);
21 }
22}
diff --git a/vendor/doctrine/orm/src/Query/AST/IndexBy.php b/vendor/doctrine/orm/src/Query/AST/IndexBy.php
new file mode 100644
index 0000000..3d90265
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/IndexBy.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class IndexBy extends Node
15{
16 public function __construct(public PathExpression $singleValuedPathExpression)
17 {
18 }
19
20 public function dispatch(SqlWalker $walker): string
21 {
22 $walker->walkIndexBy($this);
23
24 return '';
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/InputParameter.php b/vendor/doctrine/orm/src/Query/AST/InputParameter.php
new file mode 100644
index 0000000..a8e0a3b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/InputParameter.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\QueryException;
8use Doctrine\ORM\Query\SqlWalker;
9
10use function is_numeric;
11use function strlen;
12use function substr;
13
14class InputParameter extends Node
15{
16 public bool $isNamed;
17 public string $name;
18
19 /** @throws QueryException */
20 public function __construct(string $value)
21 {
22 if (strlen($value) === 1) {
23 throw QueryException::invalidParameterFormat($value);
24 }
25
26 $param = substr($value, 1);
27 $this->isNamed = ! is_numeric($param);
28 $this->name = $param;
29 }
30
31 public function dispatch(SqlWalker $walker): string
32 {
33 return $walker->walkInputParameter($this);
34 }
35}
diff --git a/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php
new file mode 100644
index 0000000..3a4e75f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
11 * InstanceOfParameter ::= AbstractSchemaName | InputParameter
12 *
13 * @link www.doctrine-project.org
14 */
15class InstanceOfExpression extends Node
16{
17 /** @param non-empty-list<InputParameter|string> $value */
18 public function __construct(
19 public string $identificationVariable,
20 public array $value,
21 public bool $not = false,
22 ) {
23 }
24
25 public function dispatch(SqlWalker $walker): string
26 {
27 return $walker->walkInstanceOfExpression($this);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/Join.php b/vendor/doctrine/orm/src/Query/AST/Join.php
new file mode 100644
index 0000000..34ce830
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Join.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression
11 * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression]
12 *
13 * @link www.doctrine-project.org
14 */
15class Join extends Node
16{
17 final public const JOIN_TYPE_LEFT = 1;
18 final public const JOIN_TYPE_LEFTOUTER = 2;
19 final public const JOIN_TYPE_INNER = 3;
20
21 public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null;
22
23 /** @psalm-param self::JOIN_TYPE_* $joinType */
24 public function __construct(
25 public int $joinType,
26 public Node|null $joinAssociationDeclaration = null,
27 ) {
28 }
29
30 public function dispatch(SqlWalker $walker): string
31 {
32 return $walker->walkJoin($this);
33 }
34}
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php
new file mode 100644
index 0000000..e08d7f5
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable
11 *
12 * @link www.doctrine-project.org
13 */
14class JoinAssociationDeclaration extends Node
15{
16 public function __construct(
17 public JoinAssociationPathExpression $joinAssociationPathExpression,
18 public string $aliasIdentificationVariable,
19 public IndexBy|null $indexBy,
20 ) {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkJoinAssociationDeclaration($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php
new file mode 100644
index 0000000..230be36
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7/**
8 * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField)
9 *
10 * @link www.doctrine-project.org
11 */
12class JoinAssociationPathExpression extends Node
13{
14 public function __construct(
15 public string $identificationVariable,
16 public string $associationField,
17 ) {
18 }
19}
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php
new file mode 100644
index 0000000..cc92782
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
11 *
12 * @link www.doctrine-project.org
13 */
14class JoinClassPathExpression extends Node
15{
16 public function __construct(
17 public mixed $abstractSchemaName,
18 public mixed $aliasIdentificationVariable,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkJoinPathExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php
new file mode 100644
index 0000000..bf76695
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * JoinVariableDeclaration ::= Join [IndexBy]
11 *
12 * @link www.doctrine-project.org
13 */
14class JoinVariableDeclaration extends Node
15{
16 public function __construct(public Join $join, public IndexBy|null $indexBy)
17 {
18 }
19
20 public function dispatch(SqlWalker $walker): string
21 {
22 return $walker->walkJoinVariableDeclaration($this);
23 }
24}
diff --git a/vendor/doctrine/orm/src/Query/AST/LikeExpression.php b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php
new file mode 100644
index 0000000..e3f67f8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\AST\Functions\FunctionNode;
8use Doctrine\ORM\Query\SqlWalker;
9
10/**
11 * LikeExpression ::= StringExpression ["NOT"] "LIKE" string ["ESCAPE" char]
12 *
13 * @link www.doctrine-project.org
14 */
15class LikeExpression extends Node
16{
17 public function __construct(
18 public Node|string $stringExpression,
19 public InputParameter|FunctionNode|PathExpression|Literal $stringPattern,
20 public Literal|null $escapeChar = null,
21 public bool $not = false,
22 ) {
23 }
24
25 public function dispatch(SqlWalker $walker): string
26 {
27 return $walker->walkLikeExpression($this);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/Literal.php b/vendor/doctrine/orm/src/Query/AST/Literal.php
new file mode 100644
index 0000000..9ec2036
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Literal.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9class Literal extends Node
10{
11 final public const STRING = 1;
12 final public const BOOLEAN = 2;
13 final public const NUMERIC = 3;
14
15 /** @psalm-param self::* $type */
16 public function __construct(
17 public int $type,
18 public mixed $value,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkLiteral($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php
new file mode 100644
index 0000000..7383c48
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class NewObjectExpression extends Node
15{
16 /** @param mixed[] $args */
17 public function __construct(public string $className, public array $args)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkNewObject($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/Node.php b/vendor/doctrine/orm/src/Query/AST/Node.php
new file mode 100644
index 0000000..cdb5855
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Node.php
@@ -0,0 +1,85 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8use Stringable;
9
10use function get_debug_type;
11use function get_object_vars;
12use function is_array;
13use function is_object;
14use function str_repeat;
15use function var_export;
16
17use const PHP_EOL;
18
19/**
20 * Abstract class of an AST node.
21 *
22 * @link www.doctrine-project.org
23 */
24abstract class Node implements Stringable
25{
26 /**
27 * Double-dispatch method, supposed to dispatch back to the walker.
28 *
29 * Implementation is not mandatory for all nodes.
30 *
31 * @throws ASTException
32 */
33 public function dispatch(SqlWalker $walker): string
34 {
35 throw ASTException::noDispatchForNode($this);
36 }
37
38 /**
39 * Dumps the AST Node into a string representation for information purpose only.
40 */
41 public function __toString(): string
42 {
43 return $this->dump($this);
44 }
45
46 public function dump(mixed $value): string
47 {
48 static $ident = 0;
49
50 $str = '';
51
52 if ($value instanceof Node) {
53 $str .= get_debug_type($value) . '(' . PHP_EOL;
54 $props = get_object_vars($value);
55
56 foreach ($props as $name => $prop) {
57 $ident += 4;
58 $str .= str_repeat(' ', $ident) . '"' . $name . '": '
59 . $this->dump($prop) . ',' . PHP_EOL;
60 $ident -= 4;
61 }
62
63 $str .= str_repeat(' ', $ident) . ')';
64 } elseif (is_array($value)) {
65 $ident += 4;
66 $str .= 'array(';
67 $some = false;
68
69 foreach ($value as $k => $v) {
70 $str .= PHP_EOL . str_repeat(' ', $ident) . '"'
71 . $k . '" => ' . $this->dump($v) . ',';
72 $some = true;
73 }
74
75 $ident -= 4;
76 $str .= ($some ? PHP_EOL . str_repeat(' ', $ident) : '') . ')';
77 } elseif (is_object($value)) {
78 $str .= 'instanceof(' . get_debug_type($value) . ')';
79 } else {
80 $str .= var_export($value, true);
81 }
82
83 return $str;
84 }
85}
diff --git a/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php
new file mode 100644
index 0000000..e60cb04
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL"
11 *
12 * @link www.doctrine-project.org
13 */
14class NullComparisonExpression extends Node
15{
16 public function __construct(
17 public Node|string $expression,
18 public bool $not = false,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkNullComparisonExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php
new file mode 100644
index 0000000..6fffeeb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
11 *
12 * @link www.doctrine-project.org
13 */
14class NullIfExpression extends Node
15{
16 public function __construct(public mixed $firstExpression, public mixed $secondExpression)
17 {
18 }
19
20 public function dispatch(SqlWalker $walker): string
21 {
22 return $walker->walkNullIfExpression($this);
23 }
24}
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByClause.php b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php
new file mode 100644
index 0000000..f6d7a67
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
11 *
12 * @link www.doctrine-project.org
13 */
14class OrderByClause extends Node
15{
16 /** @param OrderByItem[] $orderByItems */
17 public function __construct(public array $orderByItems)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkOrderByClause($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByItem.php b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php
new file mode 100644
index 0000000..64b3f40
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php
@@ -0,0 +1,38 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9use function strtoupper;
10
11/**
12 * OrderByItem ::= (ResultVariable | StateFieldPathExpression) ["ASC" | "DESC"]
13 *
14 * @link www.doctrine-project.org
15 */
16class OrderByItem extends Node
17{
18 public string $type;
19
20 public function __construct(public mixed $expression)
21 {
22 }
23
24 public function isAsc(): bool
25 {
26 return strtoupper($this->type) === 'ASC';
27 }
28
29 public function isDesc(): bool
30 {
31 return strtoupper($this->type) === 'DESC';
32 }
33
34 public function dispatch(SqlWalker $walker): string
35 {
36 return $walker->walkOrderByItem($this);
37 }
38}
diff --git a/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php
new file mode 100644
index 0000000..cda6d19
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php
@@ -0,0 +1,22 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * ParenthesisExpression ::= "(" ArithmeticPrimary ")"
11 */
12class ParenthesisExpression extends Node
13{
14 public function __construct(public Node $expression)
15 {
16 }
17
18 public function dispatch(SqlWalker $walker): string
19 {
20 return $walker->walkParenthesisExpression($this);
21 }
22}
diff --git a/vendor/doctrine/orm/src/Query/AST/PathExpression.php b/vendor/doctrine/orm/src/Query/AST/PathExpression.php
new file mode 100644
index 0000000..4a56fcd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/PathExpression.php
@@ -0,0 +1,39 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
11 * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
12 * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression
13 * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
14 * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
15 * StateField ::= {EmbeddedClassStateField "."}* SimpleStateField
16 * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField
17 */
18class PathExpression extends Node
19{
20 final public const TYPE_COLLECTION_VALUED_ASSOCIATION = 2;
21 final public const TYPE_SINGLE_VALUED_ASSOCIATION = 4;
22 final public const TYPE_STATE_FIELD = 8;
23
24 /** @psalm-var self::TYPE_*|null */
25 public int|null $type = null;
26
27 /** @psalm-param int-mask-of<self::TYPE_*> $expectedType */
28 public function __construct(
29 public int $expectedType,
30 public string $identificationVariable,
31 public string|null $field = null,
32 ) {
33 }
34
35 public function dispatch(SqlWalker $walker): string
36 {
37 return $walker->walkPathExpression($this);
38 }
39}
diff --git a/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php
new file mode 100644
index 0000000..276f8f8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7/**
8 * Marks types that can be used in place of a ConditionalExpression as a phase
9 * 2 optimization.
10 *
11 * @internal
12 *
13 * @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm
14 */
15interface Phase2OptimizableConditional
16{
17}
diff --git a/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php
new file mode 100644
index 0000000..90331cd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php
@@ -0,0 +1,43 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9use function strtoupper;
10
11/**
12 * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
13 *
14 * @link www.doctrine-project.org
15 */
16class QuantifiedExpression extends Node
17{
18 public string $type;
19
20 public function __construct(public Subselect $subselect)
21 {
22 }
23
24 public function isAll(): bool
25 {
26 return strtoupper($this->type) === 'ALL';
27 }
28
29 public function isAny(): bool
30 {
31 return strtoupper($this->type) === 'ANY';
32 }
33
34 public function isSome(): bool
35 {
36 return strtoupper($this->type) === 'SOME';
37 }
38
39 public function dispatch(SqlWalker $walker): string
40 {
41 return $walker->walkQuantifiedExpression($this);
42 }
43}
diff --git a/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php
new file mode 100644
index 0000000..59bd5c8
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
11 *
12 * @link www.doctrine-project.org
13 */
14class RangeVariableDeclaration extends Node
15{
16 public function __construct(
17 public string $abstractSchemaName,
18 public string $aliasIdentificationVariable,
19 public bool $isRoot = true,
20 ) {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkRangeVariableDeclaration($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectClause.php b/vendor/doctrine/orm/src/Query/AST/SelectClause.php
new file mode 100644
index 0000000..ad50e67
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SelectClause.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
11 *
12 * @link www.doctrine-project.org
13 */
14class SelectClause extends Node
15{
16 /** @param mixed[] $selectExpressions */
17 public function __construct(
18 public array $selectExpressions,
19 public bool $isDistinct,
20 ) {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkSelectClause($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php
new file mode 100644
index 0000000..f09f3cd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SelectExpression ::= IdentificationVariable ["." "*"] | StateFieldPathExpression |
11 * (AggregateExpression | "(" Subselect ")") [["AS"] ["HIDDEN"] FieldAliasIdentificationVariable]
12 *
13 * @link www.doctrine-project.org
14 */
15class SelectExpression extends Node
16{
17 public function __construct(
18 public mixed $expression,
19 public string|null $fieldIdentificationVariable,
20 public bool $hiddenAliasResultVariable = false,
21 ) {
22 }
23
24 public function dispatch(SqlWalker $walker): string
25 {
26 return $walker->walkSelectExpression($this);
27 }
28}
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectStatement.php b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php
new file mode 100644
index 0000000..399462f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
11 *
12 * @link www.doctrine-project.org
13 */
14class SelectStatement extends Node
15{
16 public WhereClause|null $whereClause = null;
17
18 public GroupByClause|null $groupByClause = null;
19
20 public HavingClause|null $havingClause = null;
21
22 public OrderByClause|null $orderByClause = null;
23
24 public function __construct(public SelectClause $selectClause, public FromClause $fromClause)
25 {
26 }
27
28 public function dispatch(SqlWalker $walker): string
29 {
30 return $walker->walkSelectStatement($this);
31 }
32}
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php
new file mode 100644
index 0000000..ae7ca44
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
11 *
12 * @link www.doctrine-project.org
13 */
14class SimpleArithmeticExpression extends Node
15{
16 /** @param mixed[] $arithmeticTerms */
17 public function __construct(public array $arithmeticTerms)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkSimpleArithmeticExpression($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php
new file mode 100644
index 0000000..b3764ba
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
11 *
12 * @link www.doctrine-project.org
13 */
14class SimpleCaseExpression extends Node
15{
16 /** @param mixed[] $simpleWhenClauses */
17 public function __construct(
18 public PathExpression|null $caseOperand = null,
19 public array $simpleWhenClauses = [],
20 public mixed $elseScalarExpression = null,
21 ) {
22 }
23
24 public function dispatch(SqlWalker $walker): string
25 {
26 return $walker->walkSimpleCaseExpression($this);
27 }
28}
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php
new file mode 100644
index 0000000..0259e3b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class SimpleSelectClause extends Node
15{
16 public function __construct(
17 public SimpleSelectExpression $simpleSelectExpression,
18 public bool $isDistinct = false,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkSimpleSelectClause($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php
new file mode 100644
index 0000000..97e8f08
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable
11 * | (AggregateExpression [["AS"] FieldAliasIdentificationVariable])
12 *
13 * @link www.doctrine-project.org
14 */
15class SimpleSelectExpression extends Node
16{
17 public string|null $fieldIdentificationVariable = null;
18
19 public function __construct(public Node|string $expression)
20 {
21 }
22
23 public function dispatch(SqlWalker $walker): string
24 {
25 return $walker->walkSimpleSelectExpression($this);
26 }
27}
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php
new file mode 100644
index 0000000..892165a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class SimpleWhenClause extends Node
15{
16 public function __construct(
17 public mixed $caseScalarExpression = null,
18 public mixed $thenScalarExpression = null,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkWhenClauseExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/Subselect.php b/vendor/doctrine/orm/src/Query/AST/Subselect.php
new file mode 100644
index 0000000..8ff8595
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/Subselect.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
11 *
12 * @link www.doctrine-project.org
13 */
14class Subselect extends Node
15{
16 public WhereClause|null $whereClause = null;
17
18 public GroupByClause|null $groupByClause = null;
19
20 public HavingClause|null $havingClause = null;
21
22 public OrderByClause|null $orderByClause = null;
23
24 public function __construct(public SimpleSelectClause $simpleSelectClause, public SubselectFromClause $subselectFromClause)
25 {
26 }
27
28 public function dispatch(SqlWalker $walker): string
29 {
30 return $walker->walkSubselect($this);
31 }
32}
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php
new file mode 100644
index 0000000..7cf01e2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
11 *
12 * @link www.doctrine-project.org
13 */
14class SubselectFromClause extends Node
15{
16 /** @param mixed[] $identificationVariableDeclarations */
17 public function __construct(public array $identificationVariableDeclarations)
18 {
19 }
20
21 public function dispatch(SqlWalker $walker): string
22 {
23 return $walker->walkSubselectFromClause($this);
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php
new file mode 100644
index 0000000..eadf6bc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7/**
8 * SubselectIdentificationVariableDeclaration ::= AssociationPathExpression ["AS"] AliasIdentificationVariable
9 *
10 * @link www.doctrine-project.org
11 */
12class SubselectIdentificationVariableDeclaration
13{
14 public function __construct(
15 public PathExpression $associationPathExpression,
16 public string $aliasIdentificationVariable,
17 ) {
18 }
19}
diff --git a/vendor/doctrine/orm/src/Query/AST/TypedExpression.php b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php
new file mode 100644
index 0000000..d7cf76f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\DBAL\Types\Type;
8
9/**
10 * Provides an API for resolving the type of a Node
11 */
12interface TypedExpression
13{
14 public function getReturnType(): Type;
15}
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateClause.php b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php
new file mode 100644
index 0000000..cafcff0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * UpdateClause ::= "UPDATE" AbstractSchemaName [["AS"] AliasIdentificationVariable] "SET" UpdateItem {"," UpdateItem}*
11 *
12 * @link www.doctrine-project.org
13 */
14class UpdateClause extends Node
15{
16 public string $aliasIdentificationVariable;
17
18 /** @param mixed[] $updateItems */
19 public function __construct(
20 public string $abstractSchemaName,
21 public array $updateItems,
22 ) {
23 }
24
25 public function dispatch(SqlWalker $walker): string
26 {
27 return $walker->walkUpdateClause($this);
28 }
29}
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateItem.php b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php
new file mode 100644
index 0000000..b540593
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue
11 * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
12 * EnumPrimary | SimpleEntityExpression | "NULL"
13 *
14 * @link www.doctrine-project.org
15 */
16class UpdateItem extends Node
17{
18 public function __construct(public PathExpression $pathExpression, public InputParameter|ArithmeticExpression|null $newValue)
19 {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkUpdateItem($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php
new file mode 100644
index 0000000..7ea5076
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * UpdateStatement = UpdateClause [WhereClause]
11 *
12 * @link www.doctrine-project.org
13 */
14class UpdateStatement extends Node
15{
16 public WhereClause|null $whereClause = null;
17
18 public function __construct(public UpdateClause $updateClause)
19 {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkUpdateStatement($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/WhenClause.php b/vendor/doctrine/orm/src/Query/AST/WhenClause.php
new file mode 100644
index 0000000..9bf194e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/WhenClause.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class WhenClause extends Node
15{
16 public function __construct(
17 public ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression,
18 public mixed $thenScalarExpression = null,
19 ) {
20 }
21
22 public function dispatch(SqlWalker $walker): string
23 {
24 return $walker->walkWhenClauseExpression($this);
25 }
26}
diff --git a/vendor/doctrine/orm/src/Query/AST/WhereClause.php b/vendor/doctrine/orm/src/Query/AST/WhereClause.php
new file mode 100644
index 0000000..e4d7b66
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/AST/WhereClause.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\AST;
6
7use Doctrine\ORM\Query\SqlWalker;
8
9/**
10 * WhereClause ::= "WHERE" ConditionalExpression
11 *
12 * @link www.doctrine-project.org
13 */
14class WhereClause extends Node
15{
16 public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression)
17 {
18 }
19
20 public function dispatch(SqlWalker $walker): string
21 {
22 return $walker->walkWhereClause($this);
23 }
24}
diff --git a/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php
new file mode 100644
index 0000000..101bf26
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php
@@ -0,0 +1,61 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Exec;
6
7use Doctrine\DBAL\ArrayParameterType;
8use Doctrine\DBAL\Cache\QueryCacheProfile;
9use Doctrine\DBAL\Connection;
10use Doctrine\DBAL\ParameterType;
11use Doctrine\DBAL\Result;
12use Doctrine\DBAL\Types\Type;
13
14/**
15 * Base class for SQL statement executors.
16 *
17 * @link http://www.doctrine-project.org
18 *
19 * @todo Rename: AbstractSQLExecutor
20 * @psalm-type WrapperParameterType = string|Type|ParameterType::*|ArrayParameterType::*
21 * @psalm-type WrapperParameterTypeArray = array<int<0, max>, WrapperParameterType>|array<string, WrapperParameterType>
22 */
23abstract class AbstractSqlExecutor
24{
25 /** @var list<string>|string */
26 protected array|string $sqlStatements;
27
28 protected QueryCacheProfile|null $queryCacheProfile = null;
29
30 /**
31 * Gets the SQL statements that are executed by the executor.
32 *
33 * @return list<string>|string All the SQL update statements.
34 */
35 public function getSqlStatements(): array|string
36 {
37 return $this->sqlStatements;
38 }
39
40 public function setQueryCacheProfile(QueryCacheProfile $qcp): void
41 {
42 $this->queryCacheProfile = $qcp;
43 }
44
45 /**
46 * Do not use query cache
47 */
48 public function removeQueryCacheProfile(): void
49 {
50 $this->queryCacheProfile = null;
51 }
52
53 /**
54 * Executes all sql statements.
55 *
56 * @param Connection $conn The database connection that is used to execute the queries.
57 * @param list<mixed>|array<string, mixed> $params The parameters.
58 * @psalm-param WrapperParameterTypeArray $types The parameter types.
59 */
60 abstract public function execute(Connection $conn, array $params, array $types): Result|int;
61}
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php
new file mode 100644
index 0000000..6096462
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php
@@ -0,0 +1,131 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Exec;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\ORM\Query\AST;
11use Doctrine\ORM\Query\AST\DeleteStatement;
12use Doctrine\ORM\Query\SqlWalker;
13use Doctrine\ORM\Utility\PersisterHelper;
14use Throwable;
15
16use function array_reverse;
17use function implode;
18
19/**
20 * Executes the SQL statements for bulk DQL DELETE statements on classes in
21 * Class Table Inheritance (JOINED).
22 *
23 * @link http://www.doctrine-project.org
24 */
25class MultiTableDeleteExecutor extends AbstractSqlExecutor
26{
27 private readonly string $createTempTableSql;
28 private readonly string $dropTempTableSql;
29 private readonly string $insertSql;
30
31 /**
32 * Initializes a new <tt>MultiTableDeleteExecutor</tt>.
33 *
34 * Internal note: Any SQL construction and preparation takes place in the constructor for
35 * best performance. With a query cache the executor will be cached.
36 *
37 * @param DeleteStatement $AST The root AST node of the DQL query.
38 * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST.
39 */
40 public function __construct(AST\Node $AST, SqlWalker $sqlWalker)
41 {
42 $em = $sqlWalker->getEntityManager();
43 $conn = $em->getConnection();
44 $platform = $conn->getDatabasePlatform();
45 $quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
46
47 if ($conn instanceof PrimaryReadReplicaConnection) {
48 $conn->ensureConnectedToPrimary();
49 }
50
51 $primaryClass = $em->getClassMetadata($AST->deleteClause->abstractSchemaName);
52 $primaryDqlAlias = $AST->deleteClause->aliasIdentificationVariable;
53 $rootClass = $em->getClassMetadata($primaryClass->rootEntityName);
54
55 $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
56 $idColumnNames = $rootClass->getIdentifierColumnNames();
57 $idColumnList = implode(', ', $idColumnNames);
58
59 // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause()
60 $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $primaryDqlAlias);
61
62 $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')'
63 . ' SELECT t0.' . implode(', t0.', $idColumnNames);
64
65 $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $primaryDqlAlias);
66 $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]);
67 $insertSql .= $sqlWalker->walkFromClause($fromClause);
68
69 // Append WHERE clause, if there is one.
70 if ($AST->whereClause) {
71 $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause);
72 }
73
74 $this->insertSql = $insertSql;
75
76 // 2. Create ID subselect statement used in DELETE ... WHERE ... IN (subselect)
77 $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable;
78
79 // 3. Create and store DELETE statements
80 $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses];
81 foreach (array_reverse($classNames) as $className) {
82 $tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform);
83 $this->sqlStatements[] = 'DELETE FROM ' . $tableName
84 . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
85 }
86
87 // 4. Store DDL for temporary identifier table.
88 $columnDefinitions = [];
89 foreach ($idColumnNames as $idColumnName) {
90 $columnDefinitions[$idColumnName] = [
91 'name' => $idColumnName,
92 'notnull' => true,
93 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)),
94 ];
95 }
96
97 $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' ('
98 . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))';
99 $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable);
100 }
101
102 /**
103 * {@inheritDoc}
104 */
105 public function execute(Connection $conn, array $params, array $types): int
106 {
107 // Create temporary id table
108 $conn->executeStatement($this->createTempTableSql);
109
110 try {
111 // Insert identifiers
112 $numDeleted = $conn->executeStatement($this->insertSql, $params, $types);
113
114 // Execute DELETE statements
115 foreach ($this->sqlStatements as $sql) {
116 $conn->executeStatement($sql);
117 }
118 } catch (Throwable $exception) {
119 // FAILURE! Drop temporary table to avoid possible collisions
120 $conn->executeStatement($this->dropTempTableSql);
121
122 // Re-throw exception
123 throw $exception;
124 }
125
126 // Drop temporary table
127 $conn->executeStatement($this->dropTempTableSql);
128
129 return $numDeleted;
130 }
131}
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php
new file mode 100644
index 0000000..dab1b61
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php
@@ -0,0 +1,180 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Exec;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\ORM\Query\AST;
11use Doctrine\ORM\Query\AST\UpdateStatement;
12use Doctrine\ORM\Query\ParameterTypeInferer;
13use Doctrine\ORM\Query\SqlWalker;
14use Doctrine\ORM\Utility\PersisterHelper;
15
16use function array_reverse;
17use function array_slice;
18use function implode;
19
20/**
21 * Executes the SQL statements for bulk DQL UPDATE statements on classes in
22 * Class Table Inheritance (JOINED).
23 */
24class MultiTableUpdateExecutor extends AbstractSqlExecutor
25{
26 private readonly string $createTempTableSql;
27 private readonly string $dropTempTableSql;
28 private readonly string $insertSql;
29
30 /** @var mixed[] */
31 private array $sqlParameters = [];
32 private int $numParametersInUpdateClause = 0;
33
34 /**
35 * Initializes a new <tt>MultiTableUpdateExecutor</tt>.
36 *
37 * Internal note: Any SQL construction and preparation takes place in the constructor for
38 * best performance. With a query cache the executor will be cached.
39 *
40 * @param UpdateStatement $AST The root AST node of the DQL query.
41 * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST.
42 */
43 public function __construct(AST\Node $AST, SqlWalker $sqlWalker)
44 {
45 $em = $sqlWalker->getEntityManager();
46 $conn = $em->getConnection();
47 $platform = $conn->getDatabasePlatform();
48 $quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
49 $this->sqlStatements = [];
50
51 if ($conn instanceof PrimaryReadReplicaConnection) {
52 $conn->ensureConnectedToPrimary();
53 }
54
55 $updateClause = $AST->updateClause;
56 $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName);
57 $rootClass = $em->getClassMetadata($primaryClass->rootEntityName);
58
59 $updateItems = $updateClause->updateItems;
60
61 $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
62 $idColumnNames = $rootClass->getIdentifierColumnNames();
63 $idColumnList = implode(', ', $idColumnNames);
64
65 // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause()
66 $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $updateClause->aliasIdentificationVariable);
67
68 $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')'
69 . ' SELECT t0.' . implode(', t0.', $idColumnNames);
70
71 $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable);
72 $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]);
73
74 $insertSql .= $sqlWalker->walkFromClause($fromClause);
75
76 // 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect)
77 $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable;
78
79 // 3. Create and store UPDATE statements
80 $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses];
81
82 foreach (array_reverse($classNames) as $className) {
83 $affected = false;
84 $class = $em->getClassMetadata($className);
85 $updateSql = 'UPDATE ' . $quoteStrategy->getTableName($class, $platform) . ' SET ';
86
87 $sqlParameters = [];
88 foreach ($updateItems as $updateItem) {
89 $field = $updateItem->pathExpression->field;
90
91 if (
92 (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]->inherited)) ||
93 (isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]->inherited))
94 ) {
95 $newValue = $updateItem->newValue;
96
97 if (! $affected) {
98 $affected = true;
99 } else {
100 $updateSql .= ', ';
101 }
102
103 $updateSql .= $sqlWalker->walkUpdateItem($updateItem);
104
105 if ($newValue instanceof AST\InputParameter) {
106 $sqlParameters[] = $newValue->name;
107
108 ++$this->numParametersInUpdateClause;
109 }
110 }
111 }
112
113 if ($affected) {
114 $this->sqlParameters[] = $sqlParameters;
115 $this->sqlStatements[] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
116 }
117 }
118
119 // Append WHERE clause to insertSql, if there is one.
120 if ($AST->whereClause) {
121 $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause);
122 }
123
124 $this->insertSql = $insertSql;
125
126 // 4. Store DDL for temporary identifier table.
127 $columnDefinitions = [];
128
129 foreach ($idColumnNames as $idColumnName) {
130 $columnDefinitions[$idColumnName] = [
131 'name' => $idColumnName,
132 'notnull' => true,
133 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)),
134 ];
135 }
136
137 $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' ('
138 . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))';
139
140 $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable);
141 }
142
143 /**
144 * {@inheritDoc}
145 */
146 public function execute(Connection $conn, array $params, array $types): int
147 {
148 // Create temporary id table
149 $conn->executeStatement($this->createTempTableSql);
150
151 try {
152 // Insert identifiers. Parameters from the update clause are cut off.
153 $numUpdated = $conn->executeStatement(
154 $this->insertSql,
155 array_slice($params, $this->numParametersInUpdateClause),
156 array_slice($types, $this->numParametersInUpdateClause),
157 );
158
159 // Execute UPDATE statements
160 foreach ($this->sqlStatements as $key => $statement) {
161 $paramValues = [];
162 $paramTypes = [];
163
164 if (isset($this->sqlParameters[$key])) {
165 foreach ($this->sqlParameters[$key] as $parameterKey => $parameterName) {
166 $paramValues[] = $params[$parameterKey];
167 $paramTypes[] = $types[$parameterKey] ?? ParameterTypeInferer::inferType($params[$parameterKey]);
168 }
169 }
170
171 $conn->executeStatement($statement, $paramValues, $paramTypes);
172 }
173 } finally {
174 // Drop temporary table
175 $conn->executeStatement($this->dropTempTableSql);
176 }
177
178 return $numUpdated;
179 }
180}
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php
new file mode 100644
index 0000000..5445edb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php
@@ -0,0 +1,31 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Exec;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Result;
9use Doctrine\ORM\Query\AST\SelectStatement;
10use Doctrine\ORM\Query\SqlWalker;
11
12/**
13 * Executor that executes the SQL statement for simple DQL SELECT statements.
14 *
15 * @link www.doctrine-project.org
16 */
17class SingleSelectExecutor extends AbstractSqlExecutor
18{
19 public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
20 {
21 $this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
22 }
23
24 /**
25 * {@inheritDoc}
26 */
27 public function execute(Connection $conn, array $params, array $types): Result
28 {
29 return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile);
30 }
31}
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php
new file mode 100644
index 0000000..66696db
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php
@@ -0,0 +1,42 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Exec;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
9use Doctrine\ORM\Query\AST;
10use Doctrine\ORM\Query\SqlWalker;
11
12/**
13 * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes
14 * that are mapped to a single table.
15 *
16 * @link www.doctrine-project.org
17 *
18 * @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
19 */
20class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
21{
22 public function __construct(AST\Node $AST, SqlWalker $sqlWalker)
23 {
24 if ($AST instanceof AST\UpdateStatement) {
25 $this->sqlStatements = $sqlWalker->walkUpdateStatement($AST);
26 } elseif ($AST instanceof AST\DeleteStatement) {
27 $this->sqlStatements = $sqlWalker->walkDeleteStatement($AST);
28 }
29 }
30
31 /**
32 * {@inheritDoc}
33 */
34 public function execute(Connection $conn, array $params, array $types): int
35 {
36 if ($conn instanceof PrimaryReadReplicaConnection) {
37 $conn->ensureConnectedToPrimary();
38 }
39
40 return $conn->executeStatement($this->sqlStatements, $params, $types);
41 }
42}
diff --git a/vendor/doctrine/orm/src/Query/Expr.php b/vendor/doctrine/orm/src/Query/Expr.php
new file mode 100644
index 0000000..65f3082
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr.php
@@ -0,0 +1,615 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\Internal\NoUnknownNamedArguments;
8use Traversable;
9
10use function implode;
11use function is_bool;
12use function is_float;
13use function is_int;
14use function is_iterable;
15use function iterator_to_array;
16use function str_replace;
17
18/**
19 * This class is used to generate DQL expressions via a set of PHP static functions.
20 *
21 * @link www.doctrine-project.org
22 *
23 * @todo Rename: ExpressionBuilder
24 */
25class Expr
26{
27 use NoUnknownNamedArguments;
28
29 /**
30 * Creates a conjunction of the given boolean expressions.
31 *
32 * Example:
33 *
34 * [php]
35 * // (u.type = ?1) AND (u.role = ?2)
36 * $expr->andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2'));
37 *
38 * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null,
39 * but requires at least one defined
40 * when converting to string.
41 */
42 public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx
43 {
44 self::validateVariadicParameter($x);
45
46 return new Expr\Andx($x);
47 }
48
49 /**
50 * Creates a disjunction of the given boolean expressions.
51 *
52 * Example:
53 *
54 * [php]
55 * // (u.type = ?1) OR (u.role = ?2)
56 * $q->where($q->expr()->orX('u.type = ?1', 'u.role = ?2'));
57 *
58 * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null,
59 * but requires at least one defined
60 * when converting to string.
61 */
62 public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx
63 {
64 self::validateVariadicParameter($x);
65
66 return new Expr\Orx($x);
67 }
68
69 /**
70 * Creates an ASCending order expression.
71 */
72 public function asc(mixed $expr): Expr\OrderBy
73 {
74 return new Expr\OrderBy($expr, 'ASC');
75 }
76
77 /**
78 * Creates a DESCending order expression.
79 */
80 public function desc(mixed $expr): Expr\OrderBy
81 {
82 return new Expr\OrderBy($expr, 'DESC');
83 }
84
85 /**
86 * Creates an equality comparison expression with the given arguments.
87 *
88 * First argument is considered the left expression and the second is the right expression.
89 * When converted to string, it will generated a <left expr> = <right expr>. Example:
90 *
91 * [php]
92 * // u.id = ?1
93 * $expr->eq('u.id', '?1');
94 *
95 * @param mixed $x Left expression.
96 * @param mixed $y Right expression.
97 */
98 public function eq(mixed $x, mixed $y): Expr\Comparison
99 {
100 return new Expr\Comparison($x, Expr\Comparison::EQ, $y);
101 }
102
103 /**
104 * Creates an instance of Expr\Comparison, with the given arguments.
105 * First argument is considered the left expression and the second is the right expression.
106 * When converted to string, it will generated a <left expr> <> <right expr>. Example:
107 *
108 * [php]
109 * // u.id <> ?1
110 * $q->where($q->expr()->neq('u.id', '?1'));
111 *
112 * @param mixed $x Left expression.
113 * @param mixed $y Right expression.
114 */
115 public function neq(mixed $x, mixed $y): Expr\Comparison
116 {
117 return new Expr\Comparison($x, Expr\Comparison::NEQ, $y);
118 }
119
120 /**
121 * Creates an instance of Expr\Comparison, with the given arguments.
122 * First argument is considered the left expression and the second is the right expression.
123 * When converted to string, it will generated a <left expr> < <right expr>. Example:
124 *
125 * [php]
126 * // u.id < ?1
127 * $q->where($q->expr()->lt('u.id', '?1'));
128 *
129 * @param mixed $x Left expression.
130 * @param mixed $y Right expression.
131 */
132 public function lt(mixed $x, mixed $y): Expr\Comparison
133 {
134 return new Expr\Comparison($x, Expr\Comparison::LT, $y);
135 }
136
137 /**
138 * Creates an instance of Expr\Comparison, with the given arguments.
139 * First argument is considered the left expression and the second is the right expression.
140 * When converted to string, it will generated a <left expr> <= <right expr>. Example:
141 *
142 * [php]
143 * // u.id <= ?1
144 * $q->where($q->expr()->lte('u.id', '?1'));
145 *
146 * @param mixed $x Left expression.
147 * @param mixed $y Right expression.
148 */
149 public function lte(mixed $x, mixed $y): Expr\Comparison
150 {
151 return new Expr\Comparison($x, Expr\Comparison::LTE, $y);
152 }
153
154 /**
155 * Creates an instance of Expr\Comparison, with the given arguments.
156 * First argument is considered the left expression and the second is the right expression.
157 * When converted to string, it will generated a <left expr> > <right expr>. Example:
158 *
159 * [php]
160 * // u.id > ?1
161 * $q->where($q->expr()->gt('u.id', '?1'));
162 *
163 * @param mixed $x Left expression.
164 * @param mixed $y Right expression.
165 */
166 public function gt(mixed $x, mixed $y): Expr\Comparison
167 {
168 return new Expr\Comparison($x, Expr\Comparison::GT, $y);
169 }
170
171 /**
172 * Creates an instance of Expr\Comparison, with the given arguments.
173 * First argument is considered the left expression and the second is the right expression.
174 * When converted to string, it will generated a <left expr> >= <right expr>. Example:
175 *
176 * [php]
177 * // u.id >= ?1
178 * $q->where($q->expr()->gte('u.id', '?1'));
179 *
180 * @param mixed $x Left expression.
181 * @param mixed $y Right expression.
182 */
183 public function gte(mixed $x, mixed $y): Expr\Comparison
184 {
185 return new Expr\Comparison($x, Expr\Comparison::GTE, $y);
186 }
187
188 /**
189 * Creates an instance of AVG() function, with the given argument.
190 *
191 * @param mixed $x Argument to be used in AVG() function.
192 */
193 public function avg(mixed $x): Expr\Func
194 {
195 return new Expr\Func('AVG', [$x]);
196 }
197
198 /**
199 * Creates an instance of MAX() function, with the given argument.
200 *
201 * @param mixed $x Argument to be used in MAX() function.
202 */
203 public function max(mixed $x): Expr\Func
204 {
205 return new Expr\Func('MAX', [$x]);
206 }
207
208 /**
209 * Creates an instance of MIN() function, with the given argument.
210 *
211 * @param mixed $x Argument to be used in MIN() function.
212 */
213 public function min(mixed $x): Expr\Func
214 {
215 return new Expr\Func('MIN', [$x]);
216 }
217
218 /**
219 * Creates an instance of COUNT() function, with the given argument.
220 *
221 * @param mixed $x Argument to be used in COUNT() function.
222 */
223 public function count(mixed $x): Expr\Func
224 {
225 return new Expr\Func('COUNT', [$x]);
226 }
227
228 /**
229 * Creates an instance of COUNT(DISTINCT) function, with the given argument.
230 *
231 * @param mixed ...$x Argument to be used in COUNT(DISTINCT) function.
232 */
233 public function countDistinct(mixed ...$x): string
234 {
235 self::validateVariadicParameter($x);
236
237 return 'COUNT(DISTINCT ' . implode(', ', $x) . ')';
238 }
239
240 /**
241 * Creates an instance of EXISTS() function, with the given DQL Subquery.
242 *
243 * @param mixed $subquery DQL Subquery to be used in EXISTS() function.
244 */
245 public function exists(mixed $subquery): Expr\Func
246 {
247 return new Expr\Func('EXISTS', [$subquery]);
248 }
249
250 /**
251 * Creates an instance of ALL() function, with the given DQL Subquery.
252 *
253 * @param mixed $subquery DQL Subquery to be used in ALL() function.
254 */
255 public function all(mixed $subquery): Expr\Func
256 {
257 return new Expr\Func('ALL', [$subquery]);
258 }
259
260 /**
261 * Creates a SOME() function expression with the given DQL subquery.
262 *
263 * @param mixed $subquery DQL Subquery to be used in SOME() function.
264 */
265 public function some(mixed $subquery): Expr\Func
266 {
267 return new Expr\Func('SOME', [$subquery]);
268 }
269
270 /**
271 * Creates an ANY() function expression with the given DQL subquery.
272 *
273 * @param mixed $subquery DQL Subquery to be used in ANY() function.
274 */
275 public function any(mixed $subquery): Expr\Func
276 {
277 return new Expr\Func('ANY', [$subquery]);
278 }
279
280 /**
281 * Creates a negation expression of the given restriction.
282 *
283 * @param mixed $restriction Restriction to be used in NOT() function.
284 */
285 public function not(mixed $restriction): Expr\Func
286 {
287 return new Expr\Func('NOT', [$restriction]);
288 }
289
290 /**
291 * Creates an ABS() function expression with the given argument.
292 *
293 * @param mixed $x Argument to be used in ABS() function.
294 */
295 public function abs(mixed $x): Expr\Func
296 {
297 return new Expr\Func('ABS', [$x]);
298 }
299
300 /**
301 * Creates a MOD($x, $y) function expression to return the remainder of $x divided by $y.
302 */
303 public function mod(mixed $x, mixed $y): Expr\Func
304 {
305 return new Expr\Func('MOD', [$x, $y]);
306 }
307
308 /**
309 * Creates a product mathematical expression with the given arguments.
310 *
311 * First argument is considered the left expression and the second is the right expression.
312 * When converted to string, it will generated a <left expr> * <right expr>. Example:
313 *
314 * [php]
315 * // u.salary * u.percentAnnualSalaryIncrease
316 * $q->expr()->prod('u.salary', 'u.percentAnnualSalaryIncrease')
317 *
318 * @param mixed $x Left expression.
319 * @param mixed $y Right expression.
320 */
321 public function prod(mixed $x, mixed $y): Expr\Math
322 {
323 return new Expr\Math($x, '*', $y);
324 }
325
326 /**
327 * Creates a difference mathematical expression with the given arguments.
328 * First argument is considered the left expression and the second is the right expression.
329 * When converted to string, it will generated a <left expr> - <right expr>. Example:
330 *
331 * [php]
332 * // u.monthlySubscriptionCount - 1
333 * $q->expr()->diff('u.monthlySubscriptionCount', '1')
334 *
335 * @param mixed $x Left expression.
336 * @param mixed $y Right expression.
337 */
338 public function diff(mixed $x, mixed $y): Expr\Math
339 {
340 return new Expr\Math($x, '-', $y);
341 }
342
343 /**
344 * Creates a sum mathematical expression with the given arguments.
345 * First argument is considered the left expression and the second is the right expression.
346 * When converted to string, it will generated a <left expr> + <right expr>. Example:
347 *
348 * [php]
349 * // u.numChildren + 1
350 * $q->expr()->sum('u.numChildren', '1')
351 *
352 * @param mixed $x Left expression.
353 * @param mixed $y Right expression.
354 */
355 public function sum(mixed $x, mixed $y): Expr\Math
356 {
357 return new Expr\Math($x, '+', $y);
358 }
359
360 /**
361 * Creates a quotient mathematical expression with the given arguments.
362 * First argument is considered the left expression and the second is the right expression.
363 * When converted to string, it will generated a <left expr> / <right expr>. Example:
364 *
365 * [php]
366 * // u.total / u.period
367 * $expr->quot('u.total', 'u.period')
368 *
369 * @param mixed $x Left expression.
370 * @param mixed $y Right expression.
371 */
372 public function quot(mixed $x, mixed $y): Expr\Math
373 {
374 return new Expr\Math($x, '/', $y);
375 }
376
377 /**
378 * Creates a SQRT() function expression with the given argument.
379 *
380 * @param mixed $x Argument to be used in SQRT() function.
381 */
382 public function sqrt(mixed $x): Expr\Func
383 {
384 return new Expr\Func('SQRT', [$x]);
385 }
386
387 /**
388 * Creates an IN() expression with the given arguments.
389 *
390 * @param string $x Field in string format to be restricted by IN() function.
391 * @param mixed $y Argument to be used in IN() function.
392 */
393 public function in(string $x, mixed $y): Expr\Func
394 {
395 if (is_iterable($y)) {
396 if ($y instanceof Traversable) {
397 $y = iterator_to_array($y);
398 }
399
400 foreach ($y as &$literal) {
401 if (! ($literal instanceof Expr\Literal)) {
402 $literal = $this->quoteLiteral($literal);
403 }
404 }
405 }
406
407 return new Expr\Func($x . ' IN', (array) $y);
408 }
409
410 /**
411 * Creates a NOT IN() expression with the given arguments.
412 *
413 * @param string $x Field in string format to be restricted by NOT IN() function.
414 * @param mixed $y Argument to be used in NOT IN() function.
415 */
416 public function notIn(string $x, mixed $y): Expr\Func
417 {
418 if (is_iterable($y)) {
419 if ($y instanceof Traversable) {
420 $y = iterator_to_array($y);
421 }
422
423 foreach ($y as &$literal) {
424 if (! ($literal instanceof Expr\Literal)) {
425 $literal = $this->quoteLiteral($literal);
426 }
427 }
428 }
429
430 return new Expr\Func($x . ' NOT IN', (array) $y);
431 }
432
433 /**
434 * Creates an IS NULL expression with the given arguments.
435 *
436 * @param string $x Field in string format to be restricted by IS NULL.
437 */
438 public function isNull(string $x): string
439 {
440 return $x . ' IS NULL';
441 }
442
443 /**
444 * Creates an IS NOT NULL expression with the given arguments.
445 *
446 * @param string $x Field in string format to be restricted by IS NOT NULL.
447 */
448 public function isNotNull(string $x): string
449 {
450 return $x . ' IS NOT NULL';
451 }
452
453 /**
454 * Creates a LIKE() comparison expression with the given arguments.
455 *
456 * @param string $x Field in string format to be inspected by LIKE() comparison.
457 * @param mixed $y Argument to be used in LIKE() comparison.
458 */
459 public function like(string $x, mixed $y): Expr\Comparison
460 {
461 return new Expr\Comparison($x, 'LIKE', $y);
462 }
463
464 /**
465 * Creates a NOT LIKE() comparison expression with the given arguments.
466 *
467 * @param string $x Field in string format to be inspected by LIKE() comparison.
468 * @param mixed $y Argument to be used in LIKE() comparison.
469 */
470 public function notLike(string $x, mixed $y): Expr\Comparison
471 {
472 return new Expr\Comparison($x, 'NOT LIKE', $y);
473 }
474
475 /**
476 * Creates a CONCAT() function expression with the given arguments.
477 *
478 * @param mixed ...$x Arguments to be used in CONCAT() function.
479 */
480 public function concat(mixed ...$x): Expr\Func
481 {
482 self::validateVariadicParameter($x);
483
484 return new Expr\Func('CONCAT', $x);
485 }
486
487 /**
488 * Creates a SUBSTRING() function expression with the given arguments.
489 *
490 * @param mixed $x Argument to be used as string to be cropped by SUBSTRING() function.
491 * @param int $from Initial offset to start cropping string. May accept negative values.
492 * @param int|null $len Length of crop. May accept negative values.
493 */
494 public function substring(mixed $x, int $from, int|null $len = null): Expr\Func
495 {
496 $args = [$x, $from];
497 if ($len !== null) {
498 $args[] = $len;
499 }
500
501 return new Expr\Func('SUBSTRING', $args);
502 }
503
504 /**
505 * Creates a LOWER() function expression with the given argument.
506 *
507 * @param mixed $x Argument to be used in LOWER() function.
508 *
509 * @return Expr\Func A LOWER function expression.
510 */
511 public function lower(mixed $x): Expr\Func
512 {
513 return new Expr\Func('LOWER', [$x]);
514 }
515
516 /**
517 * Creates an UPPER() function expression with the given argument.
518 *
519 * @param mixed $x Argument to be used in UPPER() function.
520 *
521 * @return Expr\Func An UPPER function expression.
522 */
523 public function upper(mixed $x): Expr\Func
524 {
525 return new Expr\Func('UPPER', [$x]);
526 }
527
528 /**
529 * Creates a LENGTH() function expression with the given argument.
530 *
531 * @param mixed $x Argument to be used as argument of LENGTH() function.
532 *
533 * @return Expr\Func A LENGTH function expression.
534 */
535 public function length(mixed $x): Expr\Func
536 {
537 return new Expr\Func('LENGTH', [$x]);
538 }
539
540 /**
541 * Creates a literal expression of the given argument.
542 *
543 * @param scalar $literal Argument to be converted to literal.
544 */
545 public function literal(bool|string|int|float $literal): Expr\Literal
546 {
547 return new Expr\Literal($this->quoteLiteral($literal));
548 }
549
550 /**
551 * Quotes a literal value, if necessary, according to the DQL syntax.
552 *
553 * @param scalar $literal The literal value.
554 */
555 private function quoteLiteral(bool|string|int|float $literal): string
556 {
557 if (is_int($literal) || is_float($literal)) {
558 return (string) $literal;
559 }
560
561 if (is_bool($literal)) {
562 return $literal ? 'true' : 'false';
563 }
564
565 return "'" . str_replace("'", "''", $literal) . "'";
566 }
567
568 /**
569 * Creates an instance of BETWEEN() function, with the given argument.
570 *
571 * @param mixed $val Valued to be inspected by range values.
572 * @param int|string $x Starting range value to be used in BETWEEN() function.
573 * @param int|string $y End point value to be used in BETWEEN() function.
574 *
575 * @return string A BETWEEN expression.
576 */
577 public function between(mixed $val, int|string $x, int|string $y): string
578 {
579 return $val . ' BETWEEN ' . $x . ' AND ' . $y;
580 }
581
582 /**
583 * Creates an instance of TRIM() function, with the given argument.
584 *
585 * @param mixed $x Argument to be used as argument of TRIM() function.
586 *
587 * @return Expr\Func a TRIM expression.
588 */
589 public function trim(mixed $x): Expr\Func
590 {
591 return new Expr\Func('TRIM', $x);
592 }
593
594 /**
595 * Creates an instance of MEMBER OF function, with the given arguments.
596 *
597 * @param string $x Value to be checked
598 * @param string $y Value to be checked against
599 */
600 public function isMemberOf(string $x, string $y): Expr\Comparison
601 {
602 return new Expr\Comparison($x, 'MEMBER OF', $y);
603 }
604
605 /**
606 * Creates an instance of INSTANCE OF function, with the given arguments.
607 *
608 * @param string $x Value to be checked
609 * @param string $y Value to be checked against
610 */
611 public function isInstanceOf(string $x, string $y): Expr\Comparison
612 {
613 return new Expr\Comparison($x, 'INSTANCE OF', $y);
614 }
615}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Andx.php b/vendor/doctrine/orm/src/Query/Expr/Andx.php
new file mode 100644
index 0000000..a20bcef
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Andx.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7/**
8 * Expression class for building DQL and parts.
9 *
10 * @link www.doctrine-project.org
11 */
12class Andx extends Composite
13{
14 protected string $separator = ' AND ';
15
16 /** @var string[] */
17 protected array $allowedClasses = [
18 Comparison::class,
19 Func::class,
20 Orx::class,
21 self::class,
22 ];
23
24 /** @psalm-var list<string|Comparison|Func|Orx|self> */
25 protected array $parts = [];
26
27 /** @psalm-return list<string|Comparison|Func|Orx|self> */
28 public function getParts(): array
29 {
30 return $this->parts;
31 }
32}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Base.php b/vendor/doctrine/orm/src/Query/Expr/Base.php
new file mode 100644
index 0000000..e0f2572
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Base.php
@@ -0,0 +1,96 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use InvalidArgumentException;
8use Stringable;
9
10use function array_key_exists;
11use function count;
12use function get_debug_type;
13use function implode;
14use function in_array;
15use function is_array;
16use function is_string;
17use function sprintf;
18
19/**
20 * Abstract base Expr class for building DQL parts.
21 *
22 * @link www.doctrine-project.org
23 */
24abstract class Base implements Stringable
25{
26 protected string $preSeparator = '(';
27 protected string $separator = ', ';
28 protected string $postSeparator = ')';
29
30 /** @var list<class-string> */
31 protected array $allowedClasses = [];
32
33 /** @var list<string|Stringable> */
34 protected array $parts = [];
35
36 public function __construct(mixed $args = [])
37 {
38 if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) {
39 $args = $args[0];
40 }
41
42 $this->addMultiple($args);
43 }
44
45 /**
46 * @param string[]|object[]|string|object $args
47 * @psalm-param list<string|object>|string|object $args
48 *
49 * @return $this
50 */
51 public function addMultiple(array|string|object $args = []): static
52 {
53 foreach ((array) $args as $arg) {
54 $this->add($arg);
55 }
56
57 return $this;
58 }
59
60 /**
61 * @return $this
62 *
63 * @throws InvalidArgumentException
64 */
65 public function add(mixed $arg): static
66 {
67 if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) {
68 // If we decide to keep Expr\Base instances, we can use this check
69 if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) {
70 throw new InvalidArgumentException(sprintf(
71 "Expression of type '%s' not allowed in this context.",
72 get_debug_type($arg),
73 ));
74 }
75
76 $this->parts[] = $arg;
77 }
78
79 return $this;
80 }
81
82 /** @psalm-return 0|positive-int */
83 public function count(): int
84 {
85 return count($this->parts);
86 }
87
88 public function __toString(): string
89 {
90 if ($this->count() === 1) {
91 return (string) $this->parts[0];
92 }
93
94 return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator;
95 }
96}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Comparison.php b/vendor/doctrine/orm/src/Query/Expr/Comparison.php
new file mode 100644
index 0000000..ec8ef21
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Comparison.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9/**
10 * Expression class for DQL comparison expressions.
11 *
12 * @link www.doctrine-project.org
13 */
14class Comparison implements Stringable
15{
16 final public const EQ = '=';
17 final public const NEQ = '<>';
18 final public const LT = '<';
19 final public const LTE = '<=';
20 final public const GT = '>';
21 final public const GTE = '>=';
22
23 /** Creates a comparison expression with the given arguments. */
24 public function __construct(protected mixed $leftExpr, protected string $operator, protected mixed $rightExpr)
25 {
26 }
27
28 public function getLeftExpr(): mixed
29 {
30 return $this->leftExpr;
31 }
32
33 public function getOperator(): string
34 {
35 return $this->operator;
36 }
37
38 public function getRightExpr(): mixed
39 {
40 return $this->rightExpr;
41 }
42
43 public function __toString(): string
44 {
45 return $this->leftExpr . ' ' . $this->operator . ' ' . $this->rightExpr;
46 }
47}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Composite.php b/vendor/doctrine/orm/src/Query/Expr/Composite.php
new file mode 100644
index 0000000..f3007a7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Composite.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9use function implode;
10use function is_object;
11use function preg_match;
12
13/**
14 * Expression class for building DQL and parts.
15 *
16 * @link www.doctrine-project.org
17 */
18class Composite extends Base
19{
20 public function __toString(): string
21 {
22 if ($this->count() === 1) {
23 return (string) $this->parts[0];
24 }
25
26 $components = [];
27
28 foreach ($this->parts as $part) {
29 $components[] = $this->processQueryPart($part);
30 }
31
32 return implode($this->separator, $components);
33 }
34
35 private function processQueryPart(string|Stringable $part): string
36 {
37 $queryPart = (string) $part;
38
39 if (is_object($part) && $part instanceof self && $part->count() > 1) {
40 return $this->preSeparator . $queryPart . $this->postSeparator;
41 }
42
43 // Fixes DDC-1237: User may have added a where item containing nested expression (with "OR" or "AND")
44 if (preg_match('/\s(OR|AND)\s/i', $queryPart)) {
45 return $this->preSeparator . $queryPart . $this->postSeparator;
46 }
47
48 return $queryPart;
49 }
50}
diff --git a/vendor/doctrine/orm/src/Query/Expr/From.php b/vendor/doctrine/orm/src/Query/Expr/From.php
new file mode 100644
index 0000000..21af078
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/From.php
@@ -0,0 +1,48 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9/**
10 * Expression class for DQL from.
11 *
12 * @link www.doctrine-project.org
13 */
14class From implements Stringable
15{
16 /**
17 * @param class-string $from The class name.
18 * @param string $alias The alias of the class.
19 */
20 public function __construct(
21 protected string $from,
22 protected string $alias,
23 protected string|null $indexBy = null,
24 ) {
25 }
26
27 /** @return class-string */
28 public function getFrom(): string
29 {
30 return $this->from;
31 }
32
33 public function getAlias(): string
34 {
35 return $this->alias;
36 }
37
38 public function getIndexBy(): string|null
39 {
40 return $this->indexBy;
41 }
42
43 public function __toString(): string
44 {
45 return $this->from . ' ' . $this->alias .
46 ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '');
47 }
48}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Func.php b/vendor/doctrine/orm/src/Query/Expr/Func.php
new file mode 100644
index 0000000..cd9e8e0
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Func.php
@@ -0,0 +1,48 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9use function implode;
10
11/**
12 * Expression class for generating DQL functions.
13 *
14 * @link www.doctrine-project.org
15 */
16class Func implements Stringable
17{
18 /** @var mixed[] */
19 protected array $arguments;
20
21 /**
22 * Creates a function, with the given argument.
23 *
24 * @psalm-param list<mixed>|mixed $arguments
25 */
26 public function __construct(
27 protected string $name,
28 mixed $arguments,
29 ) {
30 $this->arguments = (array) $arguments;
31 }
32
33 public function getName(): string
34 {
35 return $this->name;
36 }
37
38 /** @psalm-return list<mixed> */
39 public function getArguments(): array
40 {
41 return $this->arguments;
42 }
43
44 public function __toString(): string
45 {
46 return $this->name . '(' . implode(', ', $this->arguments) . ')';
47 }
48}
diff --git a/vendor/doctrine/orm/src/Query/Expr/GroupBy.php b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php
new file mode 100644
index 0000000..fa4625a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7/**
8 * Expression class for building DQL Group By parts.
9 *
10 * @link www.doctrine-project.org
11 */
12class GroupBy extends Base
13{
14 protected string $preSeparator = '';
15 protected string $postSeparator = '';
16
17 /** @psalm-var list<string> */
18 protected array $parts = [];
19
20 /** @psalm-return list<string> */
21 public function getParts(): array
22 {
23 return $this->parts;
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Join.php b/vendor/doctrine/orm/src/Query/Expr/Join.php
new file mode 100644
index 0000000..c3b6dc9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Join.php
@@ -0,0 +1,77 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9use function strtoupper;
10
11/**
12 * Expression class for DQL join.
13 *
14 * @link www.doctrine-project.org
15 */
16class Join implements Stringable
17{
18 final public const INNER_JOIN = 'INNER';
19 final public const LEFT_JOIN = 'LEFT';
20
21 final public const ON = 'ON';
22 final public const WITH = 'WITH';
23
24 /**
25 * @psalm-param self::INNER_JOIN|self::LEFT_JOIN $joinType
26 * @psalm-param self::ON|self::WITH|null $conditionType
27 */
28 public function __construct(
29 protected string $joinType,
30 protected string $join,
31 protected string|null $alias = null,
32 protected string|null $conditionType = null,
33 protected string|Comparison|Composite|Func|null $condition = null,
34 protected string|null $indexBy = null,
35 ) {
36 }
37
38 /** @psalm-return self::INNER_JOIN|self::LEFT_JOIN */
39 public function getJoinType(): string
40 {
41 return $this->joinType;
42 }
43
44 public function getJoin(): string
45 {
46 return $this->join;
47 }
48
49 public function getAlias(): string|null
50 {
51 return $this->alias;
52 }
53
54 /** @psalm-return self::ON|self::WITH|null */
55 public function getConditionType(): string|null
56 {
57 return $this->conditionType;
58 }
59
60 public function getCondition(): string|Comparison|Composite|Func|null
61 {
62 return $this->condition;
63 }
64
65 public function getIndexBy(): string|null
66 {
67 return $this->indexBy;
68 }
69
70 public function __toString(): string
71 {
72 return strtoupper($this->joinType) . ' JOIN ' . $this->join
73 . ($this->alias ? ' ' . $this->alias : '')
74 . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '')
75 . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : '');
76 }
77}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Literal.php b/vendor/doctrine/orm/src/Query/Expr/Literal.php
new file mode 100644
index 0000000..0c13030
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Literal.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7/**
8 * Expression class for generating DQL functions.
9 *
10 * @link www.doctrine-project.org
11 */
12class Literal extends Base
13{
14 protected string $preSeparator = '';
15 protected string $postSeparator = '';
16
17 /** @psalm-var list<string> */
18 protected array $parts = [];
19
20 /** @psalm-return list<string> */
21 public function getParts(): array
22 {
23 return $this->parts;
24 }
25}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Math.php b/vendor/doctrine/orm/src/Query/Expr/Math.php
new file mode 100644
index 0000000..05e0b39
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Math.php
@@ -0,0 +1,59 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9/**
10 * Expression class for DQL math statements.
11 *
12 * @link www.doctrine-project.org
13 */
14class Math implements Stringable
15{
16 /**
17 * Creates a mathematical expression with the given arguments.
18 */
19 public function __construct(
20 protected mixed $leftExpr,
21 protected string $operator,
22 protected mixed $rightExpr,
23 ) {
24 }
25
26 public function getLeftExpr(): mixed
27 {
28 return $this->leftExpr;
29 }
30
31 public function getOperator(): string
32 {
33 return $this->operator;
34 }
35
36 public function getRightExpr(): mixed
37 {
38 return $this->rightExpr;
39 }
40
41 public function __toString(): string
42 {
43 // Adjusting Left Expression
44 $leftExpr = (string) $this->leftExpr;
45
46 if ($this->leftExpr instanceof Math) {
47 $leftExpr = '(' . $leftExpr . ')';
48 }
49
50 // Adjusting Right Expression
51 $rightExpr = (string) $this->rightExpr;
52
53 if ($this->rightExpr instanceof Math) {
54 $rightExpr = '(' . $rightExpr . ')';
55 }
56
57 return $leftExpr . ' ' . $this->operator . ' ' . $rightExpr;
58 }
59}
diff --git a/vendor/doctrine/orm/src/Query/Expr/OrderBy.php b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php
new file mode 100644
index 0000000..ac9e160
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php
@@ -0,0 +1,60 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7use Stringable;
8
9use function count;
10use function implode;
11
12/**
13 * Expression class for building DQL Order By parts.
14 *
15 * @link www.doctrine-project.org
16 */
17class OrderBy implements Stringable
18{
19 protected string $preSeparator = '';
20 protected string $separator = ', ';
21 protected string $postSeparator = '';
22
23 /** @var string[] */
24 protected array $allowedClasses = [];
25
26 /** @psalm-var list<string> */
27 protected array $parts = [];
28
29 public function __construct(
30 string|null $sort = null,
31 string|null $order = null,
32 ) {
33 if ($sort) {
34 $this->add($sort, $order);
35 }
36 }
37
38 public function add(string $sort, string|null $order = null): void
39 {
40 $order = ! $order ? 'ASC' : $order;
41 $this->parts[] = $sort . ' ' . $order;
42 }
43
44 /** @psalm-return 0|positive-int */
45 public function count(): int
46 {
47 return count($this->parts);
48 }
49
50 /** @psalm-return list<string> */
51 public function getParts(): array
52 {
53 return $this->parts;
54 }
55
56 public function __toString(): string
57 {
58 return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator;
59 }
60}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Orx.php b/vendor/doctrine/orm/src/Query/Expr/Orx.php
new file mode 100644
index 0000000..2ae2332
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Orx.php
@@ -0,0 +1,32 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7/**
8 * Expression class for building DQL OR clauses.
9 *
10 * @link www.doctrine-project.org
11 */
12class Orx extends Composite
13{
14 protected string $separator = ' OR ';
15
16 /** @var string[] */
17 protected array $allowedClasses = [
18 Comparison::class,
19 Func::class,
20 Andx::class,
21 self::class,
22 ];
23
24 /** @psalm-var list<string|Comparison|Func|Andx|self> */
25 protected array $parts = [];
26
27 /** @psalm-return list<string|Comparison|Func|Andx|self> */
28 public function getParts(): array
29 {
30 return $this->parts;
31 }
32}
diff --git a/vendor/doctrine/orm/src/Query/Expr/Select.php b/vendor/doctrine/orm/src/Query/Expr/Select.php
new file mode 100644
index 0000000..91b0b60
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Expr/Select.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Expr;
6
7/**
8 * Expression class for building DQL select statements.
9 *
10 * @link www.doctrine-project.org
11 */
12class Select extends Base
13{
14 protected string $preSeparator = '';
15 protected string $postSeparator = '';
16
17 /** @var string[] */
18 protected array $allowedClasses = [Func::class];
19
20 /** @psalm-var list<string|Func> */
21 protected array $parts = [];
22
23 /** @psalm-return list<string|Func> */
24 public function getParts(): array
25 {
26 return $this->parts;
27 }
28}
diff --git a/vendor/doctrine/orm/src/Query/Filter/FilterException.php b/vendor/doctrine/orm/src/Query/Filter/FilterException.php
new file mode 100644
index 0000000..37f12bc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Filter/FilterException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Filter;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10use function sprintf;
11
12class FilterException extends LogicException implements ORMException
13{
14 public static function cannotConvertListParameterIntoSingleValue(string $name): self
15 {
16 return new self(sprintf('Cannot convert list-based SQL filter parameter "%s" into a single value.', $name));
17 }
18
19 public static function cannotConvertSingleParameterIntoListValue(string $name): self
20 {
21 return new self(sprintf('Cannot convert single SQL filter parameter "%s" into a list value.', $name));
22 }
23}
diff --git a/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php
new file mode 100644
index 0000000..29f3775
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php
@@ -0,0 +1,174 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query\Filter;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Types\Types;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Query\ParameterTypeInferer;
12use InvalidArgumentException;
13use Stringable;
14
15use function array_map;
16use function implode;
17use function ksort;
18use function serialize;
19
20/**
21 * The base class that user defined filters should extend.
22 *
23 * Handles the setting and escaping of parameters.
24 *
25 * @abstract
26 */
27abstract class SQLFilter implements Stringable
28{
29 /**
30 * Parameters for the filter.
31 *
32 * @psalm-var array<string,array{type: string, value: mixed, is_list: bool}>
33 */
34 private array $parameters = [];
35
36 final public function __construct(
37 private readonly EntityManagerInterface $em,
38 ) {
39 }
40
41 /**
42 * Sets a parameter list that can be used by the filter.
43 *
44 * @param array<mixed> $values List of parameter values.
45 * @param string $type The parameter type. If specified, the given value will be run through
46 * the type conversion of this type.
47 *
48 * @return $this
49 */
50 final public function setParameterList(string $name, array $values, string $type = Types::STRING): static
51 {
52 $this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true];
53
54 // Keep the parameters sorted for the hash
55 ksort($this->parameters);
56
57 // The filter collection of the EM is now dirty
58 $this->em->getFilters()->setFiltersStateDirty();
59
60 return $this;
61 }
62
63 /**
64 * Sets a parameter that can be used by the filter.
65 *
66 * @param string|null $type The parameter type. If specified, the given value will be run through
67 * the type conversion of this type. This is usually not needed for
68 * strings and numeric types.
69 *
70 * @return $this
71 */
72 final public function setParameter(string $name, mixed $value, string|null $type = null): static
73 {
74 if ($type === null) {
75 $type = ParameterTypeInferer::inferType($value);
76 }
77
78 $this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false];
79
80 // Keep the parameters sorted for the hash
81 ksort($this->parameters);
82
83 // The filter collection of the EM is now dirty
84 $this->em->getFilters()->setFiltersStateDirty();
85
86 return $this;
87 }
88
89 /**
90 * Gets a parameter to use in a query.
91 *
92 * The function is responsible for the right output escaping to use the
93 * value in a query.
94 *
95 * @return string The SQL escaped parameter to use in a query.
96 *
97 * @throws InvalidArgumentException
98 */
99 final public function getParameter(string $name): string
100 {
101 if (! isset($this->parameters[$name])) {
102 throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
103 }
104
105 if ($this->parameters[$name]['is_list']) {
106 throw FilterException::cannotConvertListParameterIntoSingleValue($name);
107 }
108
109 return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']);
110 }
111
112 /**
113 * Gets a parameter to use in a query assuming it's a list of entries.
114 *
115 * The function is responsible for the right output escaping to use the
116 * value in a query, separating each entry by comma to inline it into
117 * an IN() query part.
118 *
119 * @throws InvalidArgumentException
120 */
121 final public function getParameterList(string $name): string
122 {
123 if (! isset($this->parameters[$name])) {
124 throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
125 }
126
127 if ($this->parameters[$name]['is_list'] === false) {
128 throw FilterException::cannotConvertSingleParameterIntoListValue($name);
129 }
130
131 $param = $this->parameters[$name];
132 $connection = $this->em->getConnection();
133
134 $quoted = array_map(
135 static fn (mixed $value): string => $connection->quote((string) $value),
136 $param['value'],
137 );
138
139 return implode(',', $quoted);
140 }
141
142 /**
143 * Checks if a parameter was set for the filter.
144 */
145 final public function hasParameter(string $name): bool
146 {
147 return isset($this->parameters[$name]);
148 }
149
150 /**
151 * Returns as string representation of the SQLFilter parameters (the state).
152 */
153 final public function __toString(): string
154 {
155 return serialize($this->parameters);
156 }
157
158 /**
159 * Returns the database connection used by the entity manager
160 */
161 final protected function getConnection(): Connection
162 {
163 return $this->em->getConnection();
164 }
165
166 /**
167 * Gets the SQL query part to add to a query.
168 *
169 * @psalm-param ClassMetadata<object> $targetEntity
170 *
171 * @return string The constraint SQL if there is available, empty string otherwise.
172 */
173 abstract public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string;
174}
diff --git a/vendor/doctrine/orm/src/Query/FilterCollection.php b/vendor/doctrine/orm/src/Query/FilterCollection.php
new file mode 100644
index 0000000..3d3c576
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/FilterCollection.php
@@ -0,0 +1,260 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\Configuration;
8use Doctrine\ORM\EntityManagerInterface;
9use Doctrine\ORM\Query\Filter\SQLFilter;
10use InvalidArgumentException;
11
12use function assert;
13use function ksort;
14
15/**
16 * Collection class for all the query filters.
17 */
18class FilterCollection
19{
20 /* Filter STATES */
21
22 /**
23 * A filter object is in CLEAN state when it has no changed parameters.
24 */
25 public const FILTERS_STATE_CLEAN = 1;
26
27 /**
28 * A filter object is in DIRTY state when it has changed parameters.
29 */
30 public const FILTERS_STATE_DIRTY = 2;
31
32 private readonly Configuration $config;
33
34 /**
35 * Instances of enabled filters.
36 *
37 * @var array<string, SQLFilter>
38 */
39 private array $enabledFilters = [];
40
41 /** The filter hash from the last time the query was parsed. */
42 private string $filterHash = '';
43
44 /**
45 * Instances of suspended filters.
46 *
47 * @var SQLFilter[]
48 * @psalm-var array<string, SQLFilter>
49 */
50 private array $suspendedFilters = [];
51
52 /**
53 * The current state of this filter.
54 *
55 * @psalm-var self::FILTERS_STATE_*
56 */
57 private int $filtersState = self::FILTERS_STATE_CLEAN;
58
59 public function __construct(
60 private readonly EntityManagerInterface $em,
61 ) {
62 $this->config = $em->getConfiguration();
63 }
64
65 /**
66 * Gets all the enabled filters.
67 *
68 * @return array<string, SQLFilter> The enabled filters.
69 */
70 public function getEnabledFilters(): array
71 {
72 return $this->enabledFilters;
73 }
74
75 /**
76 * Gets all the suspended filters.
77 *
78 * @return SQLFilter[] The suspended filters.
79 * @psalm-return array<string, SQLFilter>
80 */
81 public function getSuspendedFilters(): array
82 {
83 return $this->suspendedFilters;
84 }
85
86 /**
87 * Enables a filter from the collection.
88 *
89 * @throws InvalidArgumentException If the filter does not exist.
90 */
91 public function enable(string $name): SQLFilter
92 {
93 if (! $this->has($name)) {
94 throw new InvalidArgumentException("Filter '" . $name . "' does not exist.");
95 }
96
97 if (! $this->isEnabled($name)) {
98 $filterClass = $this->config->getFilterClassName($name);
99
100 assert($filterClass !== null);
101
102 $this->enabledFilters[$name] = new $filterClass($this->em);
103
104 // In case a suspended filter with the same name was forgotten
105 unset($this->suspendedFilters[$name]);
106
107 // Keep the enabled filters sorted for the hash
108 ksort($this->enabledFilters);
109
110 $this->setFiltersStateDirty();
111 }
112
113 return $this->enabledFilters[$name];
114 }
115
116 /**
117 * Disables a filter.
118 *
119 * @throws InvalidArgumentException If the filter does not exist.
120 */
121 public function disable(string $name): SQLFilter
122 {
123 // Get the filter to return it
124 $filter = $this->getFilter($name);
125
126 unset($this->enabledFilters[$name]);
127
128 $this->setFiltersStateDirty();
129
130 return $filter;
131 }
132
133 /**
134 * Suspend a filter.
135 *
136 * @param string $name Name of the filter.
137 *
138 * @return SQLFilter The suspended filter.
139 *
140 * @throws InvalidArgumentException If the filter does not exist.
141 */
142 public function suspend(string $name): SQLFilter
143 {
144 // Get the filter to return it
145 $filter = $this->getFilter($name);
146
147 $this->suspendedFilters[$name] = $filter;
148 unset($this->enabledFilters[$name]);
149
150 $this->setFiltersStateDirty();
151
152 return $filter;
153 }
154
155 /**
156 * Restore a disabled filter from the collection.
157 *
158 * @param string $name Name of the filter.
159 *
160 * @return SQLFilter The restored filter.
161 *
162 * @throws InvalidArgumentException If the filter does not exist.
163 */
164 public function restore(string $name): SQLFilter
165 {
166 if (! $this->isSuspended($name)) {
167 throw new InvalidArgumentException("Filter '" . $name . "' is not suspended.");
168 }
169
170 $this->enabledFilters[$name] = $this->suspendedFilters[$name];
171 unset($this->suspendedFilters[$name]);
172
173 // Keep the enabled filters sorted for the hash
174 ksort($this->enabledFilters);
175
176 $this->setFiltersStateDirty();
177
178 return $this->enabledFilters[$name];
179 }
180
181 /**
182 * Gets an enabled filter from the collection.
183 *
184 * @throws InvalidArgumentException If the filter is not enabled.
185 */
186 public function getFilter(string $name): SQLFilter
187 {
188 if (! $this->isEnabled($name)) {
189 throw new InvalidArgumentException("Filter '" . $name . "' is not enabled.");
190 }
191
192 return $this->enabledFilters[$name];
193 }
194
195 /**
196 * Checks whether filter with given name is defined.
197 */
198 public function has(string $name): bool
199 {
200 return $this->config->getFilterClassName($name) !== null;
201 }
202
203 /**
204 * Checks if a filter is enabled.
205 */
206 public function isEnabled(string $name): bool
207 {
208 return isset($this->enabledFilters[$name]);
209 }
210
211 /**
212 * Checks if a filter is suspended.
213 *
214 * @param string $name Name of the filter.
215 *
216 * @return bool True if the filter is suspended, false otherwise.
217 */
218 public function isSuspended(string $name): bool
219 {
220 return isset($this->suspendedFilters[$name]);
221 }
222
223 /**
224 * Checks if the filter collection is clean.
225 */
226 public function isClean(): bool
227 {
228 return $this->filtersState === self::FILTERS_STATE_CLEAN;
229 }
230
231 /**
232 * Generates a string of currently enabled filters to use for the cache id.
233 */
234 public function getHash(): string
235 {
236 // If there are only clean filters, the previous hash can be returned
237 if ($this->filtersState === self::FILTERS_STATE_CLEAN) {
238 return $this->filterHash;
239 }
240
241 $filterHash = '';
242
243 foreach ($this->enabledFilters as $name => $filter) {
244 $filterHash .= $name . $filter;
245 }
246
247 $this->filterHash = $filterHash;
248 $this->filtersState = self::FILTERS_STATE_CLEAN;
249
250 return $filterHash;
251 }
252
253 /**
254 * Sets the filter state to dirty.
255 */
256 public function setFiltersStateDirty(): void
257 {
258 $this->filtersState = self::FILTERS_STATE_DIRTY;
259 }
260}
diff --git a/vendor/doctrine/orm/src/Query/Lexer.php b/vendor/doctrine/orm/src/Query/Lexer.php
new file mode 100644
index 0000000..c446675
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Lexer.php
@@ -0,0 +1,150 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\Common\Lexer\AbstractLexer;
8
9use function constant;
10use function ctype_alpha;
11use function defined;
12use function is_numeric;
13use function str_contains;
14use function str_replace;
15use function stripos;
16use function strlen;
17use function strtoupper;
18use function substr;
19
20/**
21 * Scans a DQL query for tokens.
22 *
23 * @extends AbstractLexer<TokenType, string>
24 */
25class Lexer extends AbstractLexer
26{
27 /**
28 * Creates a new query scanner object.
29 *
30 * @param string $input A query string.
31 */
32 public function __construct(string $input)
33 {
34 $this->setInput($input);
35 }
36
37 /**
38 * {@inheritDoc}
39 */
40 protected function getCatchablePatterns(): array
41 {
42 return [
43 '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name
44 '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name
45 '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers
46 "'(?:[^']|'')*'", // quoted strings
47 '\?[0-9]*|:[a-z_][a-z0-9_]*', // parameters
48 ];
49 }
50
51 /**
52 * {@inheritDoc}
53 */
54 protected function getNonCatchablePatterns(): array
55 {
56 return ['\s+', '--.*', '(.)'];
57 }
58
59 protected function getType(string &$value): TokenType
60 {
61 $type = TokenType::T_NONE;
62
63 switch (true) {
64 // Recognize numeric values
65 case is_numeric($value):
66 if (str_contains($value, '.') || stripos($value, 'e') !== false) {
67 return TokenType::T_FLOAT;
68 }
69
70 return TokenType::T_INTEGER;
71
72 // Recognize quoted strings
73 case $value[0] === "'":
74 $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2));
75
76 return TokenType::T_STRING;
77
78 // Recognize identifiers, aliased or qualified names
79 case ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\':
80 $name = 'Doctrine\ORM\Query\TokenType::T_' . strtoupper($value);
81
82 if (defined($name)) {
83 $type = constant($name);
84
85 if ($type->value > 100) {
86 return $type;
87 }
88 }
89
90 if (str_contains($value, '\\')) {
91 return TokenType::T_FULLY_QUALIFIED_NAME;
92 }
93
94 return TokenType::T_IDENTIFIER;
95
96 // Recognize input parameters
97 case $value[0] === '?' || $value[0] === ':':
98 return TokenType::T_INPUT_PARAMETER;
99
100 // Recognize symbols
101 case $value === '.':
102 return TokenType::T_DOT;
103
104 case $value === ',':
105 return TokenType::T_COMMA;
106
107 case $value === '(':
108 return TokenType::T_OPEN_PARENTHESIS;
109
110 case $value === ')':
111 return TokenType::T_CLOSE_PARENTHESIS;
112
113 case $value === '=':
114 return TokenType::T_EQUALS;
115
116 case $value === '>':
117 return TokenType::T_GREATER_THAN;
118
119 case $value === '<':
120 return TokenType::T_LOWER_THAN;
121
122 case $value === '+':
123 return TokenType::T_PLUS;
124
125 case $value === '-':
126 return TokenType::T_MINUS;
127
128 case $value === '*':
129 return TokenType::T_MULTIPLY;
130
131 case $value === '/':
132 return TokenType::T_DIVIDE;
133
134 case $value === '!':
135 return TokenType::T_NEGATE;
136
137 case $value === '{':
138 return TokenType::T_OPEN_CURLY_BRACE;
139
140 case $value === '}':
141 return TokenType::T_CLOSE_CURLY_BRACE;
142
143 // Default
144 default:
145 // Do nothing
146 }
147
148 return $type;
149 }
150}
diff --git a/vendor/doctrine/orm/src/Query/Parameter.php b/vendor/doctrine/orm/src/Query/Parameter.php
new file mode 100644
index 0000000..43eb7a4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Parameter.php
@@ -0,0 +1,89 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use function trim;
8
9/**
10 * Defines a Query Parameter.
11 *
12 * @link www.doctrine-project.org
13 */
14class Parameter
15{
16 /**
17 * Returns the internal representation of a parameter name.
18 */
19 public static function normalizeName(int|string $name): string
20 {
21 return trim((string) $name, ':');
22 }
23
24 /**
25 * The parameter name.
26 */
27 private readonly string $name;
28
29 /**
30 * The parameter value.
31 */
32 private mixed $value;
33
34 /**
35 * The parameter type.
36 */
37 private mixed $type;
38
39 /**
40 * Whether the parameter type was explicitly specified or not
41 */
42 private readonly bool $typeSpecified;
43
44 public function __construct(int|string $name, mixed $value, mixed $type = null)
45 {
46 $this->name = self::normalizeName($name);
47 $this->typeSpecified = $type !== null;
48
49 $this->setValue($value, $type);
50 }
51
52 /**
53 * Retrieves the Parameter name.
54 */
55 public function getName(): string
56 {
57 return $this->name;
58 }
59
60 /**
61 * Retrieves the Parameter value.
62 */
63 public function getValue(): mixed
64 {
65 return $this->value;
66 }
67
68 /**
69 * Retrieves the Parameter type.
70 */
71 public function getType(): mixed
72 {
73 return $this->type;
74 }
75
76 /**
77 * Defines the Parameter value.
78 */
79 public function setValue(mixed $value, mixed $type = null): void
80 {
81 $this->value = $value;
82 $this->type = $type ?: ParameterTypeInferer::inferType($value);
83 }
84
85 public function typeWasSpecified(): bool
86 {
87 return $this->typeSpecified;
88 }
89}
diff --git a/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php
new file mode 100644
index 0000000..dae28fa
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php
@@ -0,0 +1,77 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use BackedEnum;
8use DateInterval;
9use DateTimeImmutable;
10use DateTimeInterface;
11use Doctrine\DBAL\ArrayParameterType;
12use Doctrine\DBAL\ParameterType;
13use Doctrine\DBAL\Types\Types;
14
15use function current;
16use function is_array;
17use function is_bool;
18use function is_int;
19
20/**
21 * Provides an enclosed support for parameter inferring.
22 *
23 * @link www.doctrine-project.org
24 */
25final class ParameterTypeInferer
26{
27 /**
28 * Infers type of a given value, returning a compatible constant:
29 * - Type (\Doctrine\DBAL\Types\Type::*)
30 * - Connection (\Doctrine\DBAL\Connection::PARAM_*)
31 */
32 public static function inferType(mixed $value): ParameterType|ArrayParameterType|int|string
33 {
34 if (is_int($value)) {
35 return Types::INTEGER;
36 }
37
38 if (is_bool($value)) {
39 return Types::BOOLEAN;
40 }
41
42 if ($value instanceof DateTimeImmutable) {
43 return Types::DATETIME_IMMUTABLE;
44 }
45
46 if ($value instanceof DateTimeInterface) {
47 return Types::DATETIME_MUTABLE;
48 }
49
50 if ($value instanceof DateInterval) {
51 return Types::DATEINTERVAL;
52 }
53
54 if ($value instanceof BackedEnum) {
55 return is_int($value->value)
56 ? Types::INTEGER
57 : Types::STRING;
58 }
59
60 if (is_array($value)) {
61 $firstValue = current($value);
62 if ($firstValue instanceof BackedEnum) {
63 $firstValue = $firstValue->value;
64 }
65
66 return is_int($firstValue)
67 ? ArrayParameterType::INTEGER
68 : ArrayParameterType::STRING;
69 }
70
71 return ParameterType::STRING;
72 }
73
74 private function __construct()
75 {
76 }
77}
diff --git a/vendor/doctrine/orm/src/Query/Parser.php b/vendor/doctrine/orm/src/Query/Parser.php
new file mode 100644
index 0000000..e948f2c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Parser.php
@@ -0,0 +1,3269 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\Common\Lexer\Token;
8use Doctrine\ORM\EntityManagerInterface;
9use Doctrine\ORM\Mapping\AssociationMapping;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Query;
12use Doctrine\ORM\Query\AST\Functions;
13use LogicException;
14use ReflectionClass;
15
16use function array_search;
17use function assert;
18use function class_exists;
19use function count;
20use function implode;
21use function in_array;
22use function interface_exists;
23use function is_string;
24use function sprintf;
25use function str_contains;
26use function strlen;
27use function strpos;
28use function strrpos;
29use function strtolower;
30use function substr;
31
32/**
33 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
34 * Parses a DQL query, reports any errors in it, and generates an AST.
35 *
36 * @psalm-type DqlToken = Token<TokenType, string>
37 * @psalm-type QueryComponent = array{
38 * metadata?: ClassMetadata<object>,
39 * parent?: string|null,
40 * relation?: AssociationMapping|null,
41 * map?: string|null,
42 * resultVariable?: AST\Node|string,
43 * nestingLevel: int,
44 * token: DqlToken,
45 * }
46 */
47final class Parser
48{
49 /**
50 * @readonly Maps BUILT-IN string function names to AST class names.
51 * @psalm-var array<string, class-string<Functions\FunctionNode>>
52 */
53 private static array $stringFunctions = [
54 'concat' => Functions\ConcatFunction::class,
55 'substring' => Functions\SubstringFunction::class,
56 'trim' => Functions\TrimFunction::class,
57 'lower' => Functions\LowerFunction::class,
58 'upper' => Functions\UpperFunction::class,
59 'identity' => Functions\IdentityFunction::class,
60 ];
61
62 /**
63 * @readonly Maps BUILT-IN numeric function names to AST class names.
64 * @psalm-var array<string, class-string<Functions\FunctionNode>>
65 */
66 private static array $numericFunctions = [
67 'length' => Functions\LengthFunction::class,
68 'locate' => Functions\LocateFunction::class,
69 'abs' => Functions\AbsFunction::class,
70 'sqrt' => Functions\SqrtFunction::class,
71 'mod' => Functions\ModFunction::class,
72 'size' => Functions\SizeFunction::class,
73 'date_diff' => Functions\DateDiffFunction::class,
74 'bit_and' => Functions\BitAndFunction::class,
75 'bit_or' => Functions\BitOrFunction::class,
76
77 // Aggregate functions
78 'min' => Functions\MinFunction::class,
79 'max' => Functions\MaxFunction::class,
80 'avg' => Functions\AvgFunction::class,
81 'sum' => Functions\SumFunction::class,
82 'count' => Functions\CountFunction::class,
83 ];
84
85 /**
86 * @readonly Maps BUILT-IN datetime function names to AST class names.
87 * @psalm-var array<string, class-string<Functions\FunctionNode>>
88 */
89 private static array $datetimeFunctions = [
90 'current_date' => Functions\CurrentDateFunction::class,
91 'current_time' => Functions\CurrentTimeFunction::class,
92 'current_timestamp' => Functions\CurrentTimestampFunction::class,
93 'date_add' => Functions\DateAddFunction::class,
94 'date_sub' => Functions\DateSubFunction::class,
95 ];
96
97 /*
98 * Expressions that were encountered during parsing of identifiers and expressions
99 * and still need to be validated.
100 */
101
102 /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
103 private array $deferredIdentificationVariables = [];
104
105 /** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */
106 private array $deferredPathExpressions = [];
107
108 /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */
109 private array $deferredResultVariables = [];
110
111 /** @psalm-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */
112 private array $deferredNewObjectExpressions = [];
113
114 /**
115 * The lexer.
116 */
117 private readonly Lexer $lexer;
118
119 /**
120 * The parser result.
121 */
122 private readonly ParserResult $parserResult;
123
124 /**
125 * The EntityManager.
126 */
127 private readonly EntityManagerInterface $em;
128
129 /**
130 * Map of declared query components in the parsed query.
131 *
132 * @psalm-var array<string, QueryComponent>
133 */
134 private array $queryComponents = [];
135
136 /**
137 * Keeps the nesting level of defined ResultVariables.
138 */
139 private int $nestingLevel = 0;
140
141 /**
142 * Any additional custom tree walkers that modify the AST.
143 *
144 * @psalm-var list<class-string<TreeWalker>>
145 */
146 private array $customTreeWalkers = [];
147
148 /**
149 * The custom last tree walker, if any, that is responsible for producing the output.
150 *
151 * @var class-string<SqlWalker>|null
152 */
153 private $customOutputWalker;
154
155 /** @psalm-var array<string, AST\SelectExpression> */
156 private array $identVariableExpressions = [];
157
158 /**
159 * Creates a new query parser object.
160 *
161 * @param Query $query The Query to parse.
162 */
163 public function __construct(private readonly Query $query)
164 {
165 $this->em = $query->getEntityManager();
166 $this->lexer = new Lexer((string) $query->getDQL());
167 $this->parserResult = new ParserResult();
168 }
169
170 /**
171 * Sets a custom tree walker that produces output.
172 * This tree walker will be run last over the AST, after any other walkers.
173 *
174 * @psalm-param class-string<SqlWalker> $className
175 */
176 public function setCustomOutputTreeWalker(string $className): void
177 {
178 $this->customOutputWalker = $className;
179 }
180
181 /**
182 * Adds a custom tree walker for modifying the AST.
183 *
184 * @psalm-param class-string<TreeWalker> $className
185 */
186 public function addCustomTreeWalker(string $className): void
187 {
188 $this->customTreeWalkers[] = $className;
189 }
190
191 /**
192 * Gets the lexer used by the parser.
193 */
194 public function getLexer(): Lexer
195 {
196 return $this->lexer;
197 }
198
199 /**
200 * Gets the ParserResult that is being filled with information during parsing.
201 */
202 public function getParserResult(): ParserResult
203 {
204 return $this->parserResult;
205 }
206
207 /**
208 * Gets the EntityManager used by the parser.
209 */
210 public function getEntityManager(): EntityManagerInterface
211 {
212 return $this->em;
213 }
214
215 /**
216 * Parses and builds AST for the given Query.
217 */
218 public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
219 {
220 // Parse & build AST
221 $AST = $this->QueryLanguage();
222
223 // Process any deferred validations of some nodes in the AST.
224 // This also allows post-processing of the AST for modification purposes.
225 $this->processDeferredIdentificationVariables();
226
227 if ($this->deferredPathExpressions) {
228 $this->processDeferredPathExpressions();
229 }
230
231 if ($this->deferredResultVariables) {
232 $this->processDeferredResultVariables();
233 }
234
235 if ($this->deferredNewObjectExpressions) {
236 $this->processDeferredNewObjectExpressions($AST);
237 }
238
239 $this->processRootEntityAliasSelected();
240
241 // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
242 $this->fixIdentificationVariableOrder($AST);
243
244 return $AST;
245 }
246
247 /**
248 * Attempts to match the given token with the current lookahead token.
249 *
250 * If they match, updates the lookahead token; otherwise raises a syntax
251 * error.
252 *
253 * @throws QueryException If the tokens don't match.
254 */
255 public function match(TokenType $token): void
256 {
257 $lookaheadType = $this->lexer->lookahead->type ?? null;
258
259 // Short-circuit on first condition, usually types match
260 if ($lookaheadType === $token) {
261 $this->lexer->moveNext();
262
263 return;
264 }
265
266 // If parameter is not identifier (1-99) must be exact match
267 if ($token->value < TokenType::T_IDENTIFIER->value) {
268 $this->syntaxError($this->lexer->getLiteral($token));
269 }
270
271 // If parameter is keyword (200+) must be exact match
272 if ($token->value > TokenType::T_IDENTIFIER->value) {
273 $this->syntaxError($this->lexer->getLiteral($token));
274 }
275
276 // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
277 if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) {
278 $this->syntaxError($this->lexer->getLiteral($token));
279 }
280
281 $this->lexer->moveNext();
282 }
283
284 /**
285 * Frees this parser, enabling it to be reused.
286 *
287 * @param bool $deep Whether to clean peek and reset errors.
288 * @param int $position Position to reset.
289 */
290 public function free(bool $deep = false, int $position = 0): void
291 {
292 // WARNING! Use this method with care. It resets the scanner!
293 $this->lexer->resetPosition($position);
294
295 // Deep = true cleans peek and also any previously defined errors
296 if ($deep) {
297 $this->lexer->resetPeek();
298 }
299
300 $this->lexer->token = null;
301 $this->lexer->lookahead = null;
302 }
303
304 /**
305 * Parses a query string.
306 */
307 public function parse(): ParserResult
308 {
309 $AST = $this->getAST();
310
311 $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
312 if ($customWalkers !== false) {
313 $this->customTreeWalkers = $customWalkers;
314 }
315
316 $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
317 if ($customOutputWalker !== false) {
318 $this->customOutputWalker = $customOutputWalker;
319 }
320
321 // Run any custom tree walkers over the AST
322 if ($this->customTreeWalkers) {
323 $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
324
325 foreach ($this->customTreeWalkers as $walker) {
326 $treeWalkerChain->addTreeWalker($walker);
327 }
328
329 match (true) {
330 $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST),
331 $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST),
332 $AST instanceof AST\SelectStatement => $treeWalkerChain->walkSelectStatement($AST),
333 };
334
335 $this->queryComponents = $treeWalkerChain->getQueryComponents();
336 }
337
338 $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
339 $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
340
341 // Assign an SQL executor to the parser result
342 $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
343
344 return $this->parserResult;
345 }
346
347 /**
348 * Fixes order of identification variables.
349 *
350 * They have to appear in the select clause in the same order as the
351 * declarations (from ... x join ... y join ... z ...) appear in the query
352 * as the hydration process relies on that order for proper operation.
353 */
354 private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST): void
355 {
356 if (count($this->identVariableExpressions) <= 1) {
357 return;
358 }
359
360 assert($AST instanceof AST\SelectStatement);
361
362 foreach ($this->queryComponents as $dqlAlias => $qComp) {
363 if (! isset($this->identVariableExpressions[$dqlAlias])) {
364 continue;
365 }
366
367 $expr = $this->identVariableExpressions[$dqlAlias];
368 $key = array_search($expr, $AST->selectClause->selectExpressions, true);
369
370 unset($AST->selectClause->selectExpressions[$key]);
371
372 $AST->selectClause->selectExpressions[] = $expr;
373 }
374 }
375
376 /**
377 * Generates a new syntax error.
378 *
379 * @param string $expected Expected string.
380 * @param DqlToken|null $token Got token.
381 *
382 * @throws QueryException
383 */
384 public function syntaxError(string $expected = '', Token|null $token = null): never
385 {
386 if ($token === null) {
387 $token = $this->lexer->lookahead;
388 }
389
390 $tokenPos = $token->position ?? '-1';
391
392 $message = sprintf('line 0, col %d: Error: ', $tokenPos);
393 $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
394 $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value);
395
396 throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? ''));
397 }
398
399 /**
400 * Generates a new semantical error.
401 *
402 * @param string $message Optional message.
403 * @psalm-param DqlToken|null $token
404 *
405 * @throws QueryException
406 */
407 public function semanticalError(string $message = '', Token|null $token = null): never
408 {
409 if ($token === null) {
410 $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0);
411 }
412
413 // Minimum exposed chars ahead of token
414 $distance = 12;
415
416 // Find a position of a final word to display in error string
417 $dql = $this->query->getDQL();
418 $length = strlen($dql);
419 $pos = $token->position + $distance;
420 $pos = strpos($dql, ' ', $length > $pos ? $pos : $length);
421 $length = $pos !== false ? $pos - $token->position : $distance;
422
423 $tokenPos = $token->position > 0 ? $token->position : '-1';
424 $tokenStr = substr($dql, $token->position, $length);
425
426 // Building informative message
427 $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
428
429 throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
430 }
431
432 /**
433 * Peeks beyond the matched closing parenthesis and returns the first token after that one.
434 *
435 * @param bool $resetPeek Reset peek after finding the closing parenthesis.
436 *
437 * @psalm-return DqlToken|null
438 */
439 private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null
440 {
441 $token = $this->lexer->peek();
442 $numUnmatched = 1;
443
444 while ($numUnmatched > 0 && $token !== null) {
445 switch ($token->type) {
446 case TokenType::T_OPEN_PARENTHESIS:
447 ++$numUnmatched;
448 break;
449
450 case TokenType::T_CLOSE_PARENTHESIS:
451 --$numUnmatched;
452 break;
453
454 default:
455 // Do nothing
456 }
457
458 $token = $this->lexer->peek();
459 }
460
461 if ($resetPeek) {
462 $this->lexer->resetPeek();
463 }
464
465 return $token;
466 }
467
468 /**
469 * Checks if the given token indicates a mathematical operator.
470 *
471 * @psalm-param DqlToken|null $token
472 */
473 private function isMathOperator(Token|null $token): bool
474 {
475 return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true);
476 }
477
478 /**
479 * Checks if the next-next (after lookahead) token starts a function.
480 *
481 * @return bool TRUE if the next-next tokens start a function, FALSE otherwise.
482 */
483 private function isFunction(): bool
484 {
485 assert($this->lexer->lookahead !== null);
486 $lookaheadType = $this->lexer->lookahead->type;
487 $peek = $this->lexer->peek();
488
489 $this->lexer->resetPeek();
490
491 return $lookaheadType->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS;
492 }
493
494 /**
495 * Checks whether the given token type indicates an aggregate function.
496 *
497 * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
498 */
499 private function isAggregateFunction(TokenType $tokenType): bool
500 {
501 return in_array(
502 $tokenType,
503 [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT],
504 true,
505 );
506 }
507
508 /**
509 * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
510 */
511 private function isNextAllAnySome(): bool
512 {
513 assert($this->lexer->lookahead !== null);
514
515 return in_array(
516 $this->lexer->lookahead->type,
517 [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME],
518 true,
519 );
520 }
521
522 /**
523 * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
524 * It must exist in query components list.
525 */
526 private function processDeferredIdentificationVariables(): void
527 {
528 foreach ($this->deferredIdentificationVariables as $deferredItem) {
529 $identVariable = $deferredItem['expression'];
530
531 // Check if IdentificationVariable exists in queryComponents
532 if (! isset($this->queryComponents[$identVariable])) {
533 $this->semanticalError(
534 sprintf("'%s' is not defined.", $identVariable),
535 $deferredItem['token'],
536 );
537 }
538
539 $qComp = $this->queryComponents[$identVariable];
540
541 // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
542 if (! isset($qComp['metadata'])) {
543 $this->semanticalError(
544 sprintf("'%s' does not point to a Class.", $identVariable),
545 $deferredItem['token'],
546 );
547 }
548
549 // Validate if identification variable nesting level is lower or equal than the current one
550 if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
551 $this->semanticalError(
552 sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
553 $deferredItem['token'],
554 );
555 }
556 }
557 }
558
559 /**
560 * Validates that the given <tt>NewObjectExpression</tt>.
561 */
562 private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void
563 {
564 foreach ($this->deferredNewObjectExpressions as $deferredItem) {
565 $expression = $deferredItem['expression'];
566 $token = $deferredItem['token'];
567 $className = $expression->className;
568 $args = $expression->args;
569 $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
570
571 // If the namespace is not given then assumes the first FROM entity namespace
572 if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) {
573 $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
574 $fqcn = $namespace . '\\' . $className;
575
576 if (class_exists($fqcn)) {
577 $expression->className = $fqcn;
578 $className = $fqcn;
579 }
580 }
581
582 if (! class_exists($className)) {
583 $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
584 }
585
586 $class = new ReflectionClass($className);
587
588 if (! $class->isInstantiable()) {
589 $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
590 }
591
592 if ($class->getConstructor() === null) {
593 $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
594 }
595
596 if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
597 $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
598 }
599 }
600 }
601
602 /**
603 * Validates that the given <tt>ResultVariable</tt> is semantically correct.
604 * It must exist in query components list.
605 */
606 private function processDeferredResultVariables(): void
607 {
608 foreach ($this->deferredResultVariables as $deferredItem) {
609 $resultVariable = $deferredItem['expression'];
610
611 // Check if ResultVariable exists in queryComponents
612 if (! isset($this->queryComponents[$resultVariable])) {
613 $this->semanticalError(
614 sprintf("'%s' is not defined.", $resultVariable),
615 $deferredItem['token'],
616 );
617 }
618
619 $qComp = $this->queryComponents[$resultVariable];
620
621 // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
622 if (! isset($qComp['resultVariable'])) {
623 $this->semanticalError(
624 sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
625 $deferredItem['token'],
626 );
627 }
628
629 // Validate if identification variable nesting level is lower or equal than the current one
630 if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
631 $this->semanticalError(
632 sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
633 $deferredItem['token'],
634 );
635 }
636 }
637 }
638
639 /**
640 * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
641 *
642 * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
643 * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
644 * StateFieldPathExpression ::= IdentificationVariable "." StateField
645 * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
646 * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
647 */
648 private function processDeferredPathExpressions(): void
649 {
650 foreach ($this->deferredPathExpressions as $deferredItem) {
651 $pathExpression = $deferredItem['expression'];
652
653 $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable);
654
655 $field = $pathExpression->field;
656 if ($field === null) {
657 $field = $pathExpression->field = $class->identifier[0];
658 }
659
660 // Check if field or association exists
661 if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
662 $this->semanticalError(
663 'Class ' . $class->name . ' has no field or association named ' . $field,
664 $deferredItem['token'],
665 );
666 }
667
668 $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
669
670 if (isset($class->associationMappings[$field])) {
671 $assoc = $class->associationMappings[$field];
672
673 $fieldType = $assoc->isToOne()
674 ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
675 : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
676 }
677
678 // Validate if PathExpression is one of the expected types
679 $expectedType = $pathExpression->expectedType;
680
681 if (! ($expectedType & $fieldType)) {
682 // We need to recognize which was expected type(s)
683 $expectedStringTypes = [];
684
685 // Validate state field type
686 if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
687 $expectedStringTypes[] = 'StateFieldPathExpression';
688 }
689
690 // Validate single valued association (*-to-one)
691 if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
692 $expectedStringTypes[] = 'SingleValuedAssociationField';
693 }
694
695 // Validate single valued association (*-to-many)
696 if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
697 $expectedStringTypes[] = 'CollectionValuedAssociationField';
698 }
699
700 // Build the error message
701 $semanticalError = 'Invalid PathExpression. ';
702 $semanticalError .= count($expectedStringTypes) === 1
703 ? 'Must be a ' . $expectedStringTypes[0] . '.'
704 : implode(' or ', $expectedStringTypes) . ' expected.';
705
706 $this->semanticalError($semanticalError, $deferredItem['token']);
707 }
708
709 // We need to force the type in PathExpression
710 $pathExpression->type = $fieldType;
711 }
712 }
713
714 private function processRootEntityAliasSelected(): void
715 {
716 if (! count($this->identVariableExpressions)) {
717 return;
718 }
719
720 foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
721 if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) {
722 return;
723 }
724 }
725
726 $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
727 }
728
729 /**
730 * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
731 */
732 public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
733 {
734 $statement = null;
735
736 $this->lexer->moveNext();
737
738 switch ($this->lexer->lookahead->type ?? null) {
739 case TokenType::T_SELECT:
740 $statement = $this->SelectStatement();
741 break;
742
743 case TokenType::T_UPDATE:
744 $statement = $this->UpdateStatement();
745 break;
746
747 case TokenType::T_DELETE:
748 $statement = $this->DeleteStatement();
749 break;
750
751 default:
752 $this->syntaxError('SELECT, UPDATE or DELETE');
753 break;
754 }
755
756 // Check for end of string
757 if ($this->lexer->lookahead !== null) {
758 $this->syntaxError('end of string');
759 }
760
761 return $statement;
762 }
763
764 /**
765 * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
766 */
767 public function SelectStatement(): AST\SelectStatement
768 {
769 $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
770
771 $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
772 $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;
773 $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;
774 $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;
775
776 return $selectStatement;
777 }
778
779 /**
780 * UpdateStatement ::= UpdateClause [WhereClause]
781 */
782 public function UpdateStatement(): AST\UpdateStatement
783 {
784 $updateStatement = new AST\UpdateStatement($this->UpdateClause());
785
786 $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
787
788 return $updateStatement;
789 }
790
791 /**
792 * DeleteStatement ::= DeleteClause [WhereClause]
793 */
794 public function DeleteStatement(): AST\DeleteStatement
795 {
796 $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
797
798 $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
799
800 return $deleteStatement;
801 }
802
803 /**
804 * IdentificationVariable ::= identifier
805 */
806 public function IdentificationVariable(): string
807 {
808 $this->match(TokenType::T_IDENTIFIER);
809
810 assert($this->lexer->token !== null);
811 $identVariable = $this->lexer->token->value;
812
813 $this->deferredIdentificationVariables[] = [
814 'expression' => $identVariable,
815 'nestingLevel' => $this->nestingLevel,
816 'token' => $this->lexer->token,
817 ];
818
819 return $identVariable;
820 }
821
822 /**
823 * AliasIdentificationVariable = identifier
824 */
825 public function AliasIdentificationVariable(): string
826 {
827 $this->match(TokenType::T_IDENTIFIER);
828
829 assert($this->lexer->token !== null);
830 $aliasIdentVariable = $this->lexer->token->value;
831 $exists = isset($this->queryComponents[$aliasIdentVariable]);
832
833 if ($exists) {
834 $this->semanticalError(
835 sprintf("'%s' is already defined.", $aliasIdentVariable),
836 $this->lexer->token,
837 );
838 }
839
840 return $aliasIdentVariable;
841 }
842
843 /**
844 * AbstractSchemaName ::= fully_qualified_name | identifier
845 */
846 public function AbstractSchemaName(): string
847 {
848 if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) {
849 $this->match(TokenType::T_FULLY_QUALIFIED_NAME);
850 assert($this->lexer->token !== null);
851
852 return $this->lexer->token->value;
853 }
854
855 $this->match(TokenType::T_IDENTIFIER);
856 assert($this->lexer->token !== null);
857
858 return $this->lexer->token->value;
859 }
860
861 /**
862 * Validates an AbstractSchemaName, making sure the class exists.
863 *
864 * @param string $schemaName The name to validate.
865 *
866 * @throws QueryException if the name does not exist.
867 */
868 private function validateAbstractSchemaName(string $schemaName): void
869 {
870 assert($this->lexer->token !== null);
871 if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
872 $this->semanticalError(
873 sprintf("Class '%s' is not defined.", $schemaName),
874 $this->lexer->token,
875 );
876 }
877 }
878
879 /**
880 * AliasResultVariable ::= identifier
881 */
882 public function AliasResultVariable(): string
883 {
884 $this->match(TokenType::T_IDENTIFIER);
885
886 assert($this->lexer->token !== null);
887 $resultVariable = $this->lexer->token->value;
888 $exists = isset($this->queryComponents[$resultVariable]);
889
890 if ($exists) {
891 $this->semanticalError(
892 sprintf("'%s' is already defined.", $resultVariable),
893 $this->lexer->token,
894 );
895 }
896
897 return $resultVariable;
898 }
899
900 /**
901 * ResultVariable ::= identifier
902 */
903 public function ResultVariable(): string
904 {
905 $this->match(TokenType::T_IDENTIFIER);
906
907 assert($this->lexer->token !== null);
908 $resultVariable = $this->lexer->token->value;
909
910 // Defer ResultVariable validation
911 $this->deferredResultVariables[] = [
912 'expression' => $resultVariable,
913 'nestingLevel' => $this->nestingLevel,
914 'token' => $this->lexer->token,
915 ];
916
917 return $resultVariable;
918 }
919
920 /**
921 * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
922 */
923 public function JoinAssociationPathExpression(): AST\JoinAssociationPathExpression
924 {
925 $identVariable = $this->IdentificationVariable();
926
927 if (! isset($this->queryComponents[$identVariable])) {
928 $this->semanticalError(
929 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.',
930 );
931 }
932
933 $this->match(TokenType::T_DOT);
934 $this->match(TokenType::T_IDENTIFIER);
935
936 assert($this->lexer->token !== null);
937 $field = $this->lexer->token->value;
938
939 // Validate association field
940 $class = $this->getMetadataForDqlAlias($identVariable);
941
942 if (! $class->hasAssociation($field)) {
943 $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
944 }
945
946 return new AST\JoinAssociationPathExpression($identVariable, $field);
947 }
948
949 /**
950 * Parses an arbitrary path expression and defers semantical validation
951 * based on expected types.
952 *
953 * PathExpression ::= IdentificationVariable {"." identifier}*
954 *
955 * @psalm-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes
956 */
957 public function PathExpression(int $expectedTypes): AST\PathExpression
958 {
959 $identVariable = $this->IdentificationVariable();
960 $field = null;
961
962 assert($this->lexer->token !== null);
963 if ($this->lexer->isNextToken(TokenType::T_DOT)) {
964 $this->match(TokenType::T_DOT);
965 $this->match(TokenType::T_IDENTIFIER);
966
967 $field = $this->lexer->token->value;
968
969 while ($this->lexer->isNextToken(TokenType::T_DOT)) {
970 $this->match(TokenType::T_DOT);
971 $this->match(TokenType::T_IDENTIFIER);
972 $field .= '.' . $this->lexer->token->value;
973 }
974 }
975
976 // Creating AST node
977 $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
978
979 // Defer PathExpression validation if requested to be deferred
980 $this->deferredPathExpressions[] = [
981 'expression' => $pathExpr,
982 'nestingLevel' => $this->nestingLevel,
983 'token' => $this->lexer->token,
984 ];
985
986 return $pathExpr;
987 }
988
989 /**
990 * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
991 */
992 public function AssociationPathExpression(): AST\PathExpression
993 {
994 return $this->PathExpression(
995 AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
996 AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION,
997 );
998 }
999
1000 /**
1001 * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1002 */
1003 public function SingleValuedPathExpression(): AST\PathExpression
1004 {
1005 return $this->PathExpression(
1006 AST\PathExpression::TYPE_STATE_FIELD |
1007 AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
1008 );
1009 }
1010
1011 /**
1012 * StateFieldPathExpression ::= IdentificationVariable "." StateField
1013 */
1014 public function StateFieldPathExpression(): AST\PathExpression
1015 {
1016 return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1017 }
1018
1019 /**
1020 * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1021 */
1022 public function SingleValuedAssociationPathExpression(): AST\PathExpression
1023 {
1024 return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1025 }
1026
1027 /**
1028 * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1029 */
1030 public function CollectionValuedPathExpression(): AST\PathExpression
1031 {
1032 return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1033 }
1034
1035 /**
1036 * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1037 */
1038 public function SelectClause(): AST\SelectClause
1039 {
1040 $isDistinct = false;
1041 $this->match(TokenType::T_SELECT);
1042
1043 // Check for DISTINCT
1044 if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
1045 $this->match(TokenType::T_DISTINCT);
1046
1047 $isDistinct = true;
1048 }
1049
1050 // Process SelectExpressions (1..N)
1051 $selectExpressions = [];
1052 $selectExpressions[] = $this->SelectExpression();
1053
1054 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1055 $this->match(TokenType::T_COMMA);
1056
1057 $selectExpressions[] = $this->SelectExpression();
1058 }
1059
1060 return new AST\SelectClause($selectExpressions, $isDistinct);
1061 }
1062
1063 /**
1064 * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1065 */
1066 public function SimpleSelectClause(): AST\SimpleSelectClause
1067 {
1068 $isDistinct = false;
1069 $this->match(TokenType::T_SELECT);
1070
1071 if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
1072 $this->match(TokenType::T_DISTINCT);
1073
1074 $isDistinct = true;
1075 }
1076
1077 return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1078 }
1079
1080 /**
1081 * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1082 */
1083 public function UpdateClause(): AST\UpdateClause
1084 {
1085 $this->match(TokenType::T_UPDATE);
1086 assert($this->lexer->lookahead !== null);
1087
1088 $token = $this->lexer->lookahead;
1089 $abstractSchemaName = $this->AbstractSchemaName();
1090
1091 $this->validateAbstractSchemaName($abstractSchemaName);
1092
1093 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1094 $this->match(TokenType::T_AS);
1095 }
1096
1097 $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1098
1099 $class = $this->em->getClassMetadata($abstractSchemaName);
1100
1101 // Building queryComponent
1102 $queryComponent = [
1103 'metadata' => $class,
1104 'parent' => null,
1105 'relation' => null,
1106 'map' => null,
1107 'nestingLevel' => $this->nestingLevel,
1108 'token' => $token,
1109 ];
1110
1111 $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1112
1113 $this->match(TokenType::T_SET);
1114
1115 $updateItems = [];
1116 $updateItems[] = $this->UpdateItem();
1117
1118 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1119 $this->match(TokenType::T_COMMA);
1120
1121 $updateItems[] = $this->UpdateItem();
1122 }
1123
1124 $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1125 $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1126
1127 return $updateClause;
1128 }
1129
1130 /**
1131 * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1132 */
1133 public function DeleteClause(): AST\DeleteClause
1134 {
1135 $this->match(TokenType::T_DELETE);
1136
1137 if ($this->lexer->isNextToken(TokenType::T_FROM)) {
1138 $this->match(TokenType::T_FROM);
1139 }
1140
1141 assert($this->lexer->lookahead !== null);
1142 $token = $this->lexer->lookahead;
1143 $abstractSchemaName = $this->AbstractSchemaName();
1144
1145 $this->validateAbstractSchemaName($abstractSchemaName);
1146
1147 $deleteClause = new AST\DeleteClause($abstractSchemaName);
1148
1149 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1150 $this->match(TokenType::T_AS);
1151 }
1152
1153 $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER)
1154 ? $this->AliasIdentificationVariable()
1155 : 'alias_should_have_been_set';
1156
1157 $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1158 $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1159
1160 // Building queryComponent
1161 $queryComponent = [
1162 'metadata' => $class,
1163 'parent' => null,
1164 'relation' => null,
1165 'map' => null,
1166 'nestingLevel' => $this->nestingLevel,
1167 'token' => $token,
1168 ];
1169
1170 $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1171
1172 return $deleteClause;
1173 }
1174
1175 /**
1176 * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1177 */
1178 public function FromClause(): AST\FromClause
1179 {
1180 $this->match(TokenType::T_FROM);
1181
1182 $identificationVariableDeclarations = [];
1183 $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1184
1185 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1186 $this->match(TokenType::T_COMMA);
1187
1188 $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1189 }
1190
1191 return new AST\FromClause($identificationVariableDeclarations);
1192 }
1193
1194 /**
1195 * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1196 */
1197 public function SubselectFromClause(): AST\SubselectFromClause
1198 {
1199 $this->match(TokenType::T_FROM);
1200
1201 $identificationVariables = [];
1202 $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1203
1204 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1205 $this->match(TokenType::T_COMMA);
1206
1207 $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1208 }
1209
1210 return new AST\SubselectFromClause($identificationVariables);
1211 }
1212
1213 /**
1214 * WhereClause ::= "WHERE" ConditionalExpression
1215 */
1216 public function WhereClause(): AST\WhereClause
1217 {
1218 $this->match(TokenType::T_WHERE);
1219
1220 return new AST\WhereClause($this->ConditionalExpression());
1221 }
1222
1223 /**
1224 * HavingClause ::= "HAVING" ConditionalExpression
1225 */
1226 public function HavingClause(): AST\HavingClause
1227 {
1228 $this->match(TokenType::T_HAVING);
1229
1230 return new AST\HavingClause($this->ConditionalExpression());
1231 }
1232
1233 /**
1234 * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1235 */
1236 public function GroupByClause(): AST\GroupByClause
1237 {
1238 $this->match(TokenType::T_GROUP);
1239 $this->match(TokenType::T_BY);
1240
1241 $groupByItems = [$this->GroupByItem()];
1242
1243 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1244 $this->match(TokenType::T_COMMA);
1245
1246 $groupByItems[] = $this->GroupByItem();
1247 }
1248
1249 return new AST\GroupByClause($groupByItems);
1250 }
1251
1252 /**
1253 * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1254 */
1255 public function OrderByClause(): AST\OrderByClause
1256 {
1257 $this->match(TokenType::T_ORDER);
1258 $this->match(TokenType::T_BY);
1259
1260 $orderByItems = [];
1261 $orderByItems[] = $this->OrderByItem();
1262
1263 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1264 $this->match(TokenType::T_COMMA);
1265
1266 $orderByItems[] = $this->OrderByItem();
1267 }
1268
1269 return new AST\OrderByClause($orderByItems);
1270 }
1271
1272 /**
1273 * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1274 */
1275 public function Subselect(): AST\Subselect
1276 {
1277 // Increase query nesting level
1278 $this->nestingLevel++;
1279
1280 $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1281
1282 $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;
1283 $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;
1284 $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;
1285 $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;
1286
1287 // Decrease query nesting level
1288 $this->nestingLevel--;
1289
1290 return $subselect;
1291 }
1292
1293 /**
1294 * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1295 */
1296 public function UpdateItem(): AST\UpdateItem
1297 {
1298 $pathExpr = $this->SingleValuedPathExpression();
1299
1300 $this->match(TokenType::T_EQUALS);
1301
1302 return new AST\UpdateItem($pathExpr, $this->NewValue());
1303 }
1304
1305 /**
1306 * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1307 */
1308 public function GroupByItem(): string|AST\PathExpression
1309 {
1310 // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1311 $glimpse = $this->lexer->glimpse();
1312
1313 if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) {
1314 return $this->SingleValuedPathExpression();
1315 }
1316
1317 assert($this->lexer->lookahead !== null);
1318 // Still need to decide between IdentificationVariable or ResultVariable
1319 $lookaheadValue = $this->lexer->lookahead->value;
1320
1321 if (! isset($this->queryComponents[$lookaheadValue])) {
1322 $this->semanticalError('Cannot group by undefined identification or result variable.');
1323 }
1324
1325 return isset($this->queryComponents[$lookaheadValue]['metadata'])
1326 ? $this->IdentificationVariable()
1327 : $this->ResultVariable();
1328 }
1329
1330 /**
1331 * OrderByItem ::= (
1332 * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression |
1333 * ScalarExpression | ResultVariable | FunctionDeclaration
1334 * ) ["ASC" | "DESC"]
1335 */
1336 public function OrderByItem(): AST\OrderByItem
1337 {
1338 $this->lexer->peek(); // lookahead => '.'
1339 $this->lexer->peek(); // lookahead => token after '.'
1340
1341 $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1342
1343 $this->lexer->resetPeek();
1344
1345 $glimpse = $this->lexer->glimpse();
1346
1347 assert($this->lexer->lookahead !== null);
1348 $expr = match (true) {
1349 $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
1350 $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(),
1351 $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(),
1352 $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(),
1353 $this->isFunction() => $this->FunctionDeclaration(),
1354 default => $this->ResultVariable(),
1355 };
1356
1357 $type = 'ASC';
1358 $item = new AST\OrderByItem($expr);
1359
1360 switch (true) {
1361 case $this->lexer->isNextToken(TokenType::T_DESC):
1362 $this->match(TokenType::T_DESC);
1363 $type = 'DESC';
1364 break;
1365
1366 case $this->lexer->isNextToken(TokenType::T_ASC):
1367 $this->match(TokenType::T_ASC);
1368 break;
1369
1370 default:
1371 // Do nothing
1372 }
1373
1374 $item->type = $type;
1375
1376 return $item;
1377 }
1378
1379 /**
1380 * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1381 * EnumPrimary | SimpleEntityExpression | "NULL"
1382 *
1383 * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1384 * grammar that needs to be supported:
1385 *
1386 * NewValue ::= SimpleArithmeticExpression | "NULL"
1387 *
1388 * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1389 */
1390 public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null
1391 {
1392 if ($this->lexer->isNextToken(TokenType::T_NULL)) {
1393 $this->match(TokenType::T_NULL);
1394
1395 return null;
1396 }
1397
1398 if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
1399 $this->match(TokenType::T_INPUT_PARAMETER);
1400 assert($this->lexer->token !== null);
1401
1402 return new AST\InputParameter($this->lexer->token->value);
1403 }
1404
1405 return $this->ArithmeticExpression();
1406 }
1407
1408 /**
1409 * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1410 */
1411 public function IdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration
1412 {
1413 $joins = [];
1414 $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1415 $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX)
1416 ? $this->IndexBy()
1417 : null;
1418
1419 $rangeVariableDeclaration->isRoot = true;
1420
1421 while (
1422 $this->lexer->isNextToken(TokenType::T_LEFT) ||
1423 $this->lexer->isNextToken(TokenType::T_INNER) ||
1424 $this->lexer->isNextToken(TokenType::T_JOIN)
1425 ) {
1426 $joins[] = $this->Join();
1427 }
1428
1429 return new AST\IdentificationVariableDeclaration(
1430 $rangeVariableDeclaration,
1431 $indexBy,
1432 $joins,
1433 );
1434 }
1435
1436 /**
1437 * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1438 *
1439 * {Internal note: WARNING: Solution is harder than a bare implementation.
1440 * Desired EBNF support:
1441 *
1442 * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1443 *
1444 * It demands that entire SQL generation to become programmatical. This is
1445 * needed because association based subselect requires "WHERE" conditional
1446 * expressions to be injected, but there is no scope to do that. Only scope
1447 * accessible is "FROM", prohibiting an easy implementation without larger
1448 * changes.}
1449 */
1450 public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration
1451 {
1452 /*
1453 NOT YET IMPLEMENTED!
1454
1455 $glimpse = $this->lexer->glimpse();
1456
1457 if ($glimpse->type == TokenType::T_DOT) {
1458 $associationPathExpression = $this->AssociationPathExpression();
1459
1460 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1461 $this->match(TokenType::T_AS);
1462 }
1463
1464 $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1465 $identificationVariable = $associationPathExpression->identificationVariable;
1466 $field = $associationPathExpression->associationField;
1467
1468 $class = $this->queryComponents[$identificationVariable]['metadata'];
1469 $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1470
1471 // Building queryComponent
1472 $joinQueryComponent = array(
1473 'metadata' => $targetClass,
1474 'parent' => $identificationVariable,
1475 'relation' => $class->getAssociationMapping($field),
1476 'map' => null,
1477 'nestingLevel' => $this->nestingLevel,
1478 'token' => $this->lexer->lookahead
1479 );
1480
1481 $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1482
1483 return new AST\SubselectIdentificationVariableDeclaration(
1484 $associationPathExpression, $aliasIdentificationVariable
1485 );
1486 }
1487 */
1488
1489 return $this->IdentificationVariableDeclaration();
1490 }
1491
1492 /**
1493 * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1494 * (JoinAssociationDeclaration | RangeVariableDeclaration)
1495 * ["WITH" ConditionalExpression]
1496 */
1497 public function Join(): AST\Join
1498 {
1499 // Check Join type
1500 $joinType = AST\Join::JOIN_TYPE_INNER;
1501
1502 switch (true) {
1503 case $this->lexer->isNextToken(TokenType::T_LEFT):
1504 $this->match(TokenType::T_LEFT);
1505
1506 $joinType = AST\Join::JOIN_TYPE_LEFT;
1507
1508 // Possible LEFT OUTER join
1509 if ($this->lexer->isNextToken(TokenType::T_OUTER)) {
1510 $this->match(TokenType::T_OUTER);
1511
1512 $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1513 }
1514
1515 break;
1516
1517 case $this->lexer->isNextToken(TokenType::T_INNER):
1518 $this->match(TokenType::T_INNER);
1519 break;
1520
1521 default:
1522 // Do nothing
1523 }
1524
1525 $this->match(TokenType::T_JOIN);
1526
1527 $next = $this->lexer->glimpse();
1528 assert($next !== null);
1529 $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1530 $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);
1531 $join = new AST\Join($joinType, $joinDeclaration);
1532
1533 // Describe non-root join declaration
1534 if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1535 $joinDeclaration->isRoot = false;
1536 }
1537
1538 // Check for ad-hoc Join conditions
1539 if ($adhocConditions) {
1540 $this->match(TokenType::T_WITH);
1541
1542 $join->conditionalExpression = $this->ConditionalExpression();
1543 }
1544
1545 return $join;
1546 }
1547
1548 /**
1549 * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1550 *
1551 * @throws QueryException
1552 */
1553 public function RangeVariableDeclaration(): AST\RangeVariableDeclaration
1554 {
1555 if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) {
1556 $this->semanticalError('Subquery is not supported here', $this->lexer->token);
1557 }
1558
1559 $abstractSchemaName = $this->AbstractSchemaName();
1560
1561 $this->validateAbstractSchemaName($abstractSchemaName);
1562
1563 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1564 $this->match(TokenType::T_AS);
1565 }
1566
1567 assert($this->lexer->lookahead !== null);
1568 $token = $this->lexer->lookahead;
1569 $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1570 $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1571
1572 // Building queryComponent
1573 $queryComponent = [
1574 'metadata' => $classMetadata,
1575 'parent' => null,
1576 'relation' => null,
1577 'map' => null,
1578 'nestingLevel' => $this->nestingLevel,
1579 'token' => $token,
1580 ];
1581
1582 $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1583
1584 return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1585 }
1586
1587 /**
1588 * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1589 */
1590 public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration
1591 {
1592 $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1593
1594 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1595 $this->match(TokenType::T_AS);
1596 }
1597
1598 assert($this->lexer->lookahead !== null);
1599
1600 $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1601 $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null;
1602
1603 $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1604 $field = $joinAssociationPathExpression->associationField;
1605
1606 $class = $this->getMetadataForDqlAlias($identificationVariable);
1607 $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]->targetEntity);
1608
1609 // Building queryComponent
1610 $joinQueryComponent = [
1611 'metadata' => $targetClass,
1612 'parent' => $joinAssociationPathExpression->identificationVariable,
1613 'relation' => $class->getAssociationMapping($field),
1614 'map' => null,
1615 'nestingLevel' => $this->nestingLevel,
1616 'token' => $this->lexer->lookahead,
1617 ];
1618
1619 $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1620
1621 return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1622 }
1623
1624 /**
1625 * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1626 */
1627 public function NewObjectExpression(): AST\NewObjectExpression
1628 {
1629 $args = [];
1630 $this->match(TokenType::T_NEW);
1631
1632 $className = $this->AbstractSchemaName(); // note that this is not yet validated
1633 $token = $this->lexer->token;
1634
1635 $this->match(TokenType::T_OPEN_PARENTHESIS);
1636
1637 $args[] = $this->NewObjectArg();
1638
1639 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1640 $this->match(TokenType::T_COMMA);
1641
1642 $args[] = $this->NewObjectArg();
1643 }
1644
1645 $this->match(TokenType::T_CLOSE_PARENTHESIS);
1646
1647 $expression = new AST\NewObjectExpression($className, $args);
1648
1649 // Defer NewObjectExpression validation
1650 $this->deferredNewObjectExpressions[] = [
1651 'token' => $token,
1652 'expression' => $expression,
1653 'nestingLevel' => $this->nestingLevel,
1654 ];
1655
1656 return $expression;
1657 }
1658
1659 /**
1660 * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1661 */
1662 public function NewObjectArg(): mixed
1663 {
1664 assert($this->lexer->lookahead !== null);
1665 $token = $this->lexer->lookahead;
1666 $peek = $this->lexer->glimpse();
1667
1668 assert($peek !== null);
1669 if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
1670 $this->match(TokenType::T_OPEN_PARENTHESIS);
1671 $expression = $this->Subselect();
1672 $this->match(TokenType::T_CLOSE_PARENTHESIS);
1673
1674 return $expression;
1675 }
1676
1677 return $this->ScalarExpression();
1678 }
1679
1680 /**
1681 * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
1682 */
1683 public function IndexBy(): AST\IndexBy
1684 {
1685 $this->match(TokenType::T_INDEX);
1686 $this->match(TokenType::T_BY);
1687 $pathExpr = $this->SingleValuedPathExpression();
1688
1689 // Add the INDEX BY info to the query component
1690 $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1691
1692 return new AST\IndexBy($pathExpr);
1693 }
1694
1695 /**
1696 * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1697 * StateFieldPathExpression | BooleanPrimary | CaseExpression |
1698 * InstanceOfExpression
1699 *
1700 * @return mixed One of the possible expressions or subexpressions.
1701 */
1702 public function ScalarExpression(): mixed
1703 {
1704 assert($this->lexer->token !== null);
1705 assert($this->lexer->lookahead !== null);
1706 $lookahead = $this->lexer->lookahead->type;
1707 $peek = $this->lexer->glimpse();
1708
1709 switch (true) {
1710 case $lookahead === TokenType::T_INTEGER:
1711 case $lookahead === TokenType::T_FLOAT:
1712 // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 )
1713 case $lookahead === TokenType::T_MINUS:
1714 case $lookahead === TokenType::T_PLUS:
1715 return $this->SimpleArithmeticExpression();
1716
1717 case $lookahead === TokenType::T_STRING:
1718 return $this->StringPrimary();
1719
1720 case $lookahead === TokenType::T_TRUE:
1721 case $lookahead === TokenType::T_FALSE:
1722 $this->match($lookahead);
1723
1724 return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);
1725
1726 case $lookahead === TokenType::T_INPUT_PARAMETER:
1727 return match (true) {
1728 $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
1729 default => $this->InputParameter(),
1730 };
1731
1732 case $lookahead === TokenType::T_CASE:
1733 case $lookahead === TokenType::T_COALESCE:
1734 case $lookahead === TokenType::T_NULLIF:
1735 // Since NULLIF and COALESCE can be identified as a function,
1736 // we need to check these before checking for FunctionDeclaration
1737 return $this->CaseExpression();
1738
1739 case $lookahead === TokenType::T_OPEN_PARENTHESIS:
1740 return $this->SimpleArithmeticExpression();
1741
1742 // this check must be done before checking for a filed path expression
1743 case $this->isFunction():
1744 $this->lexer->peek();
1745
1746 return match (true) {
1747 $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(),
1748 default => $this->FunctionDeclaration(),
1749 };
1750
1751 // it is no function, so it must be a field path
1752 case $lookahead === TokenType::T_IDENTIFIER:
1753 $this->lexer->peek(); // lookahead => '.'
1754 $this->lexer->peek(); // lookahead => token after '.'
1755 $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1756 $this->lexer->resetPeek();
1757
1758 if ($this->isMathOperator($peek)) {
1759 return $this->SimpleArithmeticExpression();
1760 }
1761
1762 return $this->StateFieldPathExpression();
1763
1764 default:
1765 $this->syntaxError();
1766 }
1767 }
1768
1769 /**
1770 * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
1771 * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
1772 * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
1773 * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
1774 * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
1775 * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
1776 * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
1777 * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
1778 *
1779 * @return mixed One of the possible expressions or subexpressions.
1780 */
1781 public function CaseExpression(): mixed
1782 {
1783 assert($this->lexer->lookahead !== null);
1784 $lookahead = $this->lexer->lookahead->type;
1785
1786 switch ($lookahead) {
1787 case TokenType::T_NULLIF:
1788 return $this->NullIfExpression();
1789
1790 case TokenType::T_COALESCE:
1791 return $this->CoalesceExpression();
1792
1793 case TokenType::T_CASE:
1794 $this->lexer->resetPeek();
1795 $peek = $this->lexer->peek();
1796
1797 assert($peek !== null);
1798 if ($peek->type === TokenType::T_WHEN) {
1799 return $this->GeneralCaseExpression();
1800 }
1801
1802 return $this->SimpleCaseExpression();
1803
1804 default:
1805 // Do nothing
1806 break;
1807 }
1808
1809 $this->syntaxError();
1810 }
1811
1812 /**
1813 * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
1814 */
1815 public function CoalesceExpression(): AST\CoalesceExpression
1816 {
1817 $this->match(TokenType::T_COALESCE);
1818 $this->match(TokenType::T_OPEN_PARENTHESIS);
1819
1820 // Process ScalarExpressions (1..N)
1821 $scalarExpressions = [];
1822 $scalarExpressions[] = $this->ScalarExpression();
1823
1824 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
1825 $this->match(TokenType::T_COMMA);
1826
1827 $scalarExpressions[] = $this->ScalarExpression();
1828 }
1829
1830 $this->match(TokenType::T_CLOSE_PARENTHESIS);
1831
1832 return new AST\CoalesceExpression($scalarExpressions);
1833 }
1834
1835 /**
1836 * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
1837 */
1838 public function NullIfExpression(): AST\NullIfExpression
1839 {
1840 $this->match(TokenType::T_NULLIF);
1841 $this->match(TokenType::T_OPEN_PARENTHESIS);
1842
1843 $firstExpression = $this->ScalarExpression();
1844 $this->match(TokenType::T_COMMA);
1845 $secondExpression = $this->ScalarExpression();
1846
1847 $this->match(TokenType::T_CLOSE_PARENTHESIS);
1848
1849 return new AST\NullIfExpression($firstExpression, $secondExpression);
1850 }
1851
1852 /**
1853 * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
1854 */
1855 public function GeneralCaseExpression(): AST\GeneralCaseExpression
1856 {
1857 $this->match(TokenType::T_CASE);
1858
1859 // Process WhenClause (1..N)
1860 $whenClauses = [];
1861
1862 do {
1863 $whenClauses[] = $this->WhenClause();
1864 } while ($this->lexer->isNextToken(TokenType::T_WHEN));
1865
1866 $this->match(TokenType::T_ELSE);
1867 $scalarExpression = $this->ScalarExpression();
1868 $this->match(TokenType::T_END);
1869
1870 return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
1871 }
1872
1873 /**
1874 * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
1875 * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
1876 */
1877 public function SimpleCaseExpression(): AST\SimpleCaseExpression
1878 {
1879 $this->match(TokenType::T_CASE);
1880 $caseOperand = $this->StateFieldPathExpression();
1881
1882 // Process SimpleWhenClause (1..N)
1883 $simpleWhenClauses = [];
1884
1885 do {
1886 $simpleWhenClauses[] = $this->SimpleWhenClause();
1887 } while ($this->lexer->isNextToken(TokenType::T_WHEN));
1888
1889 $this->match(TokenType::T_ELSE);
1890 $scalarExpression = $this->ScalarExpression();
1891 $this->match(TokenType::T_END);
1892
1893 return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
1894 }
1895
1896 /**
1897 * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
1898 */
1899 public function WhenClause(): AST\WhenClause
1900 {
1901 $this->match(TokenType::T_WHEN);
1902 $conditionalExpression = $this->ConditionalExpression();
1903 $this->match(TokenType::T_THEN);
1904
1905 return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
1906 }
1907
1908 /**
1909 * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
1910 */
1911 public function SimpleWhenClause(): AST\SimpleWhenClause
1912 {
1913 $this->match(TokenType::T_WHEN);
1914 $conditionalExpression = $this->ScalarExpression();
1915 $this->match(TokenType::T_THEN);
1916
1917 return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
1918 }
1919
1920 /**
1921 * SelectExpression ::= (
1922 * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
1923 * "(" Subselect ")" | CaseExpression | NewObjectExpression
1924 * ) [["AS"] ["HIDDEN"] AliasResultVariable]
1925 */
1926 public function SelectExpression(): AST\SelectExpression
1927 {
1928 assert($this->lexer->lookahead !== null);
1929 $expression = null;
1930 $identVariable = null;
1931 $peek = $this->lexer->glimpse();
1932 $lookaheadType = $this->lexer->lookahead->type;
1933 assert($peek !== null);
1934
1935 switch (true) {
1936 // ScalarExpression (u.name)
1937 case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT:
1938 $expression = $this->ScalarExpression();
1939 break;
1940
1941 // IdentificationVariable (u)
1942 case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS:
1943 $expression = $identVariable = $this->IdentificationVariable();
1944 break;
1945
1946 // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
1947 case $lookaheadType === TokenType::T_CASE:
1948 case $lookaheadType === TokenType::T_COALESCE:
1949 case $lookaheadType === TokenType::T_NULLIF:
1950 $expression = $this->CaseExpression();
1951 break;
1952
1953 // DQL Function (SUM(u.value) or SUM(u.value) + 1)
1954 case $this->isFunction():
1955 $this->lexer->peek(); // "("
1956
1957 $expression = match (true) {
1958 $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(),
1959 default => $this->FunctionDeclaration(),
1960 };
1961
1962 break;
1963
1964 // Subselect
1965 case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:
1966 $this->match(TokenType::T_OPEN_PARENTHESIS);
1967 $expression = $this->Subselect();
1968 $this->match(TokenType::T_CLOSE_PARENTHESIS);
1969 break;
1970
1971 // Shortcut: ScalarExpression => SimpleArithmeticExpression
1972 case $lookaheadType === TokenType::T_OPEN_PARENTHESIS:
1973 case $lookaheadType === TokenType::T_INTEGER:
1974 case $lookaheadType === TokenType::T_STRING:
1975 case $lookaheadType === TokenType::T_FLOAT:
1976 // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
1977 case $lookaheadType === TokenType::T_MINUS:
1978 case $lookaheadType === TokenType::T_PLUS:
1979 $expression = $this->SimpleArithmeticExpression();
1980 break;
1981
1982 // NewObjectExpression (New ClassName(id, name))
1983 case $lookaheadType === TokenType::T_NEW:
1984 $expression = $this->NewObjectExpression();
1985 break;
1986
1987 default:
1988 $this->syntaxError(
1989 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression',
1990 $this->lexer->lookahead,
1991 );
1992 }
1993
1994 // [["AS"] ["HIDDEN"] AliasResultVariable]
1995 $mustHaveAliasResultVariable = false;
1996
1997 if ($this->lexer->isNextToken(TokenType::T_AS)) {
1998 $this->match(TokenType::T_AS);
1999
2000 $mustHaveAliasResultVariable = true;
2001 }
2002
2003 $hiddenAliasResultVariable = false;
2004
2005 if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) {
2006 $this->match(TokenType::T_HIDDEN);
2007
2008 $hiddenAliasResultVariable = true;
2009 }
2010
2011 $aliasResultVariable = null;
2012
2013 if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
2014 assert($expression instanceof AST\Node || is_string($expression));
2015 $token = $this->lexer->lookahead;
2016 $aliasResultVariable = $this->AliasResultVariable();
2017
2018 // Include AliasResultVariable in query components.
2019 $this->queryComponents[$aliasResultVariable] = [
2020 'resultVariable' => $expression,
2021 'nestingLevel' => $this->nestingLevel,
2022 'token' => $token,
2023 ];
2024 }
2025
2026 // AST
2027
2028 $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2029
2030 if ($identVariable) {
2031 $this->identVariableExpressions[$identVariable] = $expr;
2032 }
2033
2034 return $expr;
2035 }
2036
2037 /**
2038 * SimpleSelectExpression ::= (
2039 * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2040 * AggregateExpression | "(" Subselect ")" | ScalarExpression
2041 * ) [["AS"] AliasResultVariable]
2042 */
2043 public function SimpleSelectExpression(): AST\SimpleSelectExpression
2044 {
2045 assert($this->lexer->lookahead !== null);
2046 $peek = $this->lexer->glimpse();
2047 assert($peek !== null);
2048
2049 switch ($this->lexer->lookahead->type) {
2050 case TokenType::T_IDENTIFIER:
2051 switch (true) {
2052 case $peek->type === TokenType::T_DOT:
2053 $expression = $this->StateFieldPathExpression();
2054
2055 return new AST\SimpleSelectExpression($expression);
2056
2057 case $peek->type !== TokenType::T_OPEN_PARENTHESIS:
2058 $expression = $this->IdentificationVariable();
2059
2060 return new AST\SimpleSelectExpression($expression);
2061
2062 case $this->isFunction():
2063 // SUM(u.id) + COUNT(u.id)
2064 if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
2065 return new AST\SimpleSelectExpression($this->ScalarExpression());
2066 }
2067
2068 // COUNT(u.id)
2069 if ($this->isAggregateFunction($this->lexer->lookahead->type)) {
2070 return new AST\SimpleSelectExpression($this->AggregateExpression());
2071 }
2072
2073 // IDENTITY(u)
2074 return new AST\SimpleSelectExpression($this->FunctionDeclaration());
2075
2076 default:
2077 // Do nothing
2078 }
2079
2080 break;
2081
2082 case TokenType::T_OPEN_PARENTHESIS:
2083 if ($peek->type !== TokenType::T_SELECT) {
2084 // Shortcut: ScalarExpression => SimpleArithmeticExpression
2085 $expression = $this->SimpleArithmeticExpression();
2086
2087 return new AST\SimpleSelectExpression($expression);
2088 }
2089
2090 // Subselect
2091 $this->match(TokenType::T_OPEN_PARENTHESIS);
2092 $expression = $this->Subselect();
2093 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2094
2095 return new AST\SimpleSelectExpression($expression);
2096
2097 default:
2098 // Do nothing
2099 }
2100
2101 $this->lexer->peek();
2102
2103 $expression = $this->ScalarExpression();
2104 $expr = new AST\SimpleSelectExpression($expression);
2105
2106 if ($this->lexer->isNextToken(TokenType::T_AS)) {
2107 $this->match(TokenType::T_AS);
2108 }
2109
2110 if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
2111 $token = $this->lexer->lookahead;
2112 $resultVariable = $this->AliasResultVariable();
2113 $expr->fieldIdentificationVariable = $resultVariable;
2114
2115 // Include AliasResultVariable in query components.
2116 $this->queryComponents[$resultVariable] = [
2117 'resultvariable' => $expr,
2118 'nestingLevel' => $this->nestingLevel,
2119 'token' => $token,
2120 ];
2121 }
2122
2123 return $expr;
2124 }
2125
2126 /**
2127 * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2128 */
2129 public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm
2130 {
2131 $conditionalTerms = [];
2132 $conditionalTerms[] = $this->ConditionalTerm();
2133
2134 while ($this->lexer->isNextToken(TokenType::T_OR)) {
2135 $this->match(TokenType::T_OR);
2136
2137 $conditionalTerms[] = $this->ConditionalTerm();
2138 }
2139
2140 // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2141 // if only one AST\ConditionalTerm is defined
2142 if (count($conditionalTerms) === 1) {
2143 return $conditionalTerms[0];
2144 }
2145
2146 return new AST\ConditionalExpression($conditionalTerms);
2147 }
2148
2149 /**
2150 * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2151 */
2152 public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm
2153 {
2154 $conditionalFactors = [];
2155 $conditionalFactors[] = $this->ConditionalFactor();
2156
2157 while ($this->lexer->isNextToken(TokenType::T_AND)) {
2158 $this->match(TokenType::T_AND);
2159
2160 $conditionalFactors[] = $this->ConditionalFactor();
2161 }
2162
2163 // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2164 // if only one AST\ConditionalFactor is defined
2165 if (count($conditionalFactors) === 1) {
2166 return $conditionalFactors[0];
2167 }
2168
2169 return new AST\ConditionalTerm($conditionalFactors);
2170 }
2171
2172 /**
2173 * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2174 */
2175 public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary
2176 {
2177 $not = false;
2178
2179 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2180 $this->match(TokenType::T_NOT);
2181
2182 $not = true;
2183 }
2184
2185 $conditionalPrimary = $this->ConditionalPrimary();
2186
2187 // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2188 // if only one AST\ConditionalPrimary is defined
2189 if (! $not) {
2190 return $conditionalPrimary;
2191 }
2192
2193 return new AST\ConditionalFactor($conditionalPrimary, $not);
2194 }
2195
2196 /**
2197 * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2198 */
2199 public function ConditionalPrimary(): AST\ConditionalPrimary
2200 {
2201 $condPrimary = new AST\ConditionalPrimary();
2202
2203 if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
2204 $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2205
2206 return $condPrimary;
2207 }
2208
2209 // Peek beyond the matching closing parenthesis ')'
2210 $peek = $this->peekBeyondClosingParenthesis();
2211
2212 if (
2213 $peek !== null && (
2214 in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) ||
2215 in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) ||
2216 $this->isMathOperator($peek)
2217 )
2218 ) {
2219 $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2220
2221 return $condPrimary;
2222 }
2223
2224 $this->match(TokenType::T_OPEN_PARENTHESIS);
2225 $condPrimary->conditionalExpression = $this->ConditionalExpression();
2226 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2227
2228 return $condPrimary;
2229 }
2230
2231 /**
2232 * SimpleConditionalExpression ::=
2233 * ComparisonExpression | BetweenExpression | LikeExpression |
2234 * InExpression | NullComparisonExpression | ExistsExpression |
2235 * EmptyCollectionComparisonExpression | CollectionMemberExpression |
2236 * InstanceOfExpression
2237 */
2238 public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression
2239 {
2240 assert($this->lexer->lookahead !== null);
2241 if ($this->lexer->isNextToken(TokenType::T_EXISTS)) {
2242 return $this->ExistsExpression();
2243 }
2244
2245 $token = $this->lexer->lookahead;
2246 $peek = $this->lexer->glimpse();
2247 $lookahead = $token;
2248
2249 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2250 $token = $this->lexer->glimpse();
2251 }
2252
2253 assert($token !== null);
2254 assert($peek !== null);
2255 if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) {
2256 // Peek beyond the matching closing parenthesis.
2257 $beyond = $this->lexer->peek();
2258
2259 switch ($peek->value) {
2260 case '(':
2261 // Peeks beyond the matched closing parenthesis.
2262 $token = $this->peekBeyondClosingParenthesis(false);
2263 assert($token !== null);
2264
2265 if ($token->type === TokenType::T_NOT) {
2266 $token = $this->lexer->peek();
2267 assert($token !== null);
2268 }
2269
2270 if ($token->type === TokenType::T_IS) {
2271 $lookahead = $this->lexer->peek();
2272 }
2273
2274 break;
2275
2276 default:
2277 // Peek beyond the PathExpression or InputParameter.
2278 $token = $beyond;
2279
2280 while ($token->value === '.') {
2281 $this->lexer->peek();
2282
2283 $token = $this->lexer->peek();
2284 assert($token !== null);
2285 }
2286
2287 // Also peek beyond a NOT if there is one.
2288 assert($token !== null);
2289 if ($token->type === TokenType::T_NOT) {
2290 $token = $this->lexer->peek();
2291 assert($token !== null);
2292 }
2293
2294 // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2295 $lookahead = $this->lexer->peek();
2296 }
2297
2298 assert($lookahead !== null);
2299 // Also peek beyond a NOT if there is one.
2300 if ($lookahead->type === TokenType::T_NOT) {
2301 $lookahead = $this->lexer->peek();
2302 }
2303
2304 $this->lexer->resetPeek();
2305 }
2306
2307 if ($token->type === TokenType::T_BETWEEN) {
2308 return $this->BetweenExpression();
2309 }
2310
2311 if ($token->type === TokenType::T_LIKE) {
2312 return $this->LikeExpression();
2313 }
2314
2315 if ($token->type === TokenType::T_IN) {
2316 return $this->InExpression();
2317 }
2318
2319 if ($token->type === TokenType::T_INSTANCE) {
2320 return $this->InstanceOfExpression();
2321 }
2322
2323 if ($token->type === TokenType::T_MEMBER) {
2324 return $this->CollectionMemberExpression();
2325 }
2326
2327 assert($lookahead !== null);
2328 if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) {
2329 return $this->NullComparisonExpression();
2330 }
2331
2332 if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) {
2333 return $this->EmptyCollectionComparisonExpression();
2334 }
2335
2336 return $this->ComparisonExpression();
2337 }
2338
2339 /**
2340 * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2341 */
2342 public function EmptyCollectionComparisonExpression(): AST\EmptyCollectionComparisonExpression
2343 {
2344 $pathExpression = $this->CollectionValuedPathExpression();
2345 $this->match(TokenType::T_IS);
2346
2347 $not = false;
2348 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2349 $this->match(TokenType::T_NOT);
2350 $not = true;
2351 }
2352
2353 $this->match(TokenType::T_EMPTY);
2354
2355 return new AST\EmptyCollectionComparisonExpression(
2356 $pathExpression,
2357 $not,
2358 );
2359 }
2360
2361 /**
2362 * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2363 *
2364 * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2365 * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2366 */
2367 public function CollectionMemberExpression(): AST\CollectionMemberExpression
2368 {
2369 $not = false;
2370 $entityExpr = $this->EntityExpression();
2371
2372 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2373 $this->match(TokenType::T_NOT);
2374
2375 $not = true;
2376 }
2377
2378 $this->match(TokenType::T_MEMBER);
2379
2380 if ($this->lexer->isNextToken(TokenType::T_OF)) {
2381 $this->match(TokenType::T_OF);
2382 }
2383
2384 return new AST\CollectionMemberExpression(
2385 $entityExpr,
2386 $this->CollectionValuedPathExpression(),
2387 $not,
2388 );
2389 }
2390
2391 /**
2392 * Literal ::= string | char | integer | float | boolean
2393 */
2394 public function Literal(): AST\Literal
2395 {
2396 assert($this->lexer->lookahead !== null);
2397 assert($this->lexer->token !== null);
2398 switch ($this->lexer->lookahead->type) {
2399 case TokenType::T_STRING:
2400 $this->match(TokenType::T_STRING);
2401
2402 return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
2403
2404 case TokenType::T_INTEGER:
2405 case TokenType::T_FLOAT:
2406 $this->match(
2407 $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT,
2408 );
2409
2410 return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value);
2411
2412 case TokenType::T_TRUE:
2413 case TokenType::T_FALSE:
2414 $this->match(
2415 $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE,
2416 );
2417
2418 return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);
2419
2420 default:
2421 $this->syntaxError('Literal');
2422 }
2423 }
2424
2425 /**
2426 * InParameter ::= ArithmeticExpression | InputParameter
2427 */
2428 public function InParameter(): AST\InputParameter|AST\ArithmeticExpression
2429 {
2430 assert($this->lexer->lookahead !== null);
2431 if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) {
2432 return $this->InputParameter();
2433 }
2434
2435 return $this->ArithmeticExpression();
2436 }
2437
2438 /**
2439 * InputParameter ::= PositionalParameter | NamedParameter
2440 */
2441 public function InputParameter(): AST\InputParameter
2442 {
2443 $this->match(TokenType::T_INPUT_PARAMETER);
2444 assert($this->lexer->token !== null);
2445
2446 return new AST\InputParameter($this->lexer->token->value);
2447 }
2448
2449 /**
2450 * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2451 */
2452 public function ArithmeticExpression(): AST\ArithmeticExpression
2453 {
2454 $expr = new AST\ArithmeticExpression();
2455
2456 if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
2457 $peek = $this->lexer->glimpse();
2458 assert($peek !== null);
2459
2460 if ($peek->type === TokenType::T_SELECT) {
2461 $this->match(TokenType::T_OPEN_PARENTHESIS);
2462 $expr->subselect = $this->Subselect();
2463 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2464
2465 return $expr;
2466 }
2467 }
2468
2469 $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2470
2471 return $expr;
2472 }
2473
2474 /**
2475 * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2476 */
2477 public function SimpleArithmeticExpression(): AST\Node|string
2478 {
2479 $terms = [];
2480 $terms[] = $this->ArithmeticTerm();
2481
2482 while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) {
2483 $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);
2484
2485 assert($this->lexer->token !== null);
2486 $terms[] = $this->lexer->token->value;
2487 $terms[] = $this->ArithmeticTerm();
2488 }
2489
2490 // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2491 // if only one AST\ArithmeticTerm is defined
2492 if (count($terms) === 1) {
2493 return $terms[0];
2494 }
2495
2496 return new AST\SimpleArithmeticExpression($terms);
2497 }
2498
2499 /**
2500 * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2501 */
2502 public function ArithmeticTerm(): AST\Node|string
2503 {
2504 $factors = [];
2505 $factors[] = $this->ArithmeticFactor();
2506
2507 while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) {
2508 $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE);
2509
2510 assert($this->lexer->token !== null);
2511 $factors[] = $this->lexer->token->value;
2512 $factors[] = $this->ArithmeticFactor();
2513 }
2514
2515 // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2516 // if only one AST\ArithmeticFactor is defined
2517 if (count($factors) === 1) {
2518 return $factors[0];
2519 }
2520
2521 return new AST\ArithmeticTerm($factors);
2522 }
2523
2524 /**
2525 * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2526 */
2527 public function ArithmeticFactor(): AST\Node|string|AST\ArithmeticFactor
2528 {
2529 $sign = null;
2530
2531 $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS);
2532 if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) {
2533 $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);
2534 $sign = $isPlus;
2535 }
2536
2537 $primary = $this->ArithmeticPrimary();
2538
2539 // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2540 // if only one AST\ArithmeticPrimary is defined
2541 if ($sign === null) {
2542 return $primary;
2543 }
2544
2545 return new AST\ArithmeticFactor($primary, $sign);
2546 }
2547
2548 /**
2549 * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2550 * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2551 * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2552 * | InputParameter | CaseExpression
2553 */
2554 public function ArithmeticPrimary(): AST\Node|string
2555 {
2556 if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {
2557 $this->match(TokenType::T_OPEN_PARENTHESIS);
2558
2559 $expr = $this->SimpleArithmeticExpression();
2560
2561 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2562
2563 return new AST\ParenthesisExpression($expr);
2564 }
2565
2566 if ($this->lexer->lookahead === null) {
2567 $this->syntaxError('ArithmeticPrimary');
2568 }
2569
2570 switch ($this->lexer->lookahead->type) {
2571 case TokenType::T_COALESCE:
2572 case TokenType::T_NULLIF:
2573 case TokenType::T_CASE:
2574 return $this->CaseExpression();
2575
2576 case TokenType::T_IDENTIFIER:
2577 $peek = $this->lexer->glimpse();
2578
2579 if ($peek !== null && $peek->value === '(') {
2580 return $this->FunctionDeclaration();
2581 }
2582
2583 if ($peek !== null && $peek->value === '.') {
2584 return $this->SingleValuedPathExpression();
2585 }
2586
2587 if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) {
2588 return $this->ResultVariable();
2589 }
2590
2591 return $this->StateFieldPathExpression();
2592
2593 case TokenType::T_INPUT_PARAMETER:
2594 return $this->InputParameter();
2595
2596 default:
2597 $peek = $this->lexer->glimpse();
2598
2599 if ($peek !== null && $peek->value === '(') {
2600 return $this->FunctionDeclaration();
2601 }
2602
2603 return $this->Literal();
2604 }
2605 }
2606
2607 /**
2608 * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2609 */
2610 public function StringExpression(): AST\Subselect|AST\Node|string
2611 {
2612 $peek = $this->lexer->glimpse();
2613 assert($peek !== null);
2614
2615 // Subselect
2616 if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) {
2617 $this->match(TokenType::T_OPEN_PARENTHESIS);
2618 $expr = $this->Subselect();
2619 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2620
2621 return $expr;
2622 }
2623
2624 assert($this->lexer->lookahead !== null);
2625 // ResultVariable (string)
2626 if (
2627 $this->lexer->isNextToken(TokenType::T_IDENTIFIER) &&
2628 isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])
2629 ) {
2630 return $this->ResultVariable();
2631 }
2632
2633 return $this->StringPrimary();
2634 }
2635
2636 /**
2637 * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2638 */
2639 public function StringPrimary(): AST\Node
2640 {
2641 assert($this->lexer->lookahead !== null);
2642 $lookaheadType = $this->lexer->lookahead->type;
2643
2644 switch ($lookaheadType) {
2645 case TokenType::T_IDENTIFIER:
2646 $peek = $this->lexer->glimpse();
2647 assert($peek !== null);
2648
2649 if ($peek->value === '.') {
2650 return $this->StateFieldPathExpression();
2651 }
2652
2653 if ($peek->value === '(') {
2654 // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2655 return $this->FunctionDeclaration();
2656 }
2657
2658 $this->syntaxError("'.' or '('");
2659 break;
2660
2661 case TokenType::T_STRING:
2662 $this->match(TokenType::T_STRING);
2663 assert($this->lexer->token !== null);
2664
2665 return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
2666
2667 case TokenType::T_INPUT_PARAMETER:
2668 return $this->InputParameter();
2669
2670 case TokenType::T_CASE:
2671 case TokenType::T_COALESCE:
2672 case TokenType::T_NULLIF:
2673 return $this->CaseExpression();
2674
2675 default:
2676 assert($lookaheadType !== null);
2677 if ($this->isAggregateFunction($lookaheadType)) {
2678 return $this->AggregateExpression();
2679 }
2680 }
2681
2682 $this->syntaxError(
2683 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression',
2684 );
2685 }
2686
2687 /**
2688 * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2689 */
2690 public function EntityExpression(): AST\InputParameter|AST\PathExpression
2691 {
2692 $glimpse = $this->lexer->glimpse();
2693 assert($glimpse !== null);
2694
2695 if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') {
2696 return $this->SingleValuedAssociationPathExpression();
2697 }
2698
2699 return $this->SimpleEntityExpression();
2700 }
2701
2702 /**
2703 * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2704 */
2705 public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression
2706 {
2707 if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
2708 return $this->InputParameter();
2709 }
2710
2711 return $this->StateFieldPathExpression();
2712 }
2713
2714 /**
2715 * AggregateExpression ::=
2716 * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2717 */
2718 public function AggregateExpression(): AST\AggregateExpression
2719 {
2720 assert($this->lexer->lookahead !== null);
2721 $lookaheadType = $this->lexer->lookahead->type;
2722 $isDistinct = false;
2723
2724 if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) {
2725 $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2726 }
2727
2728 $this->match($lookaheadType);
2729 assert($this->lexer->token !== null);
2730 $functionName = $this->lexer->token->value;
2731 $this->match(TokenType::T_OPEN_PARENTHESIS);
2732
2733 if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {
2734 $this->match(TokenType::T_DISTINCT);
2735 $isDistinct = true;
2736 }
2737
2738 $pathExp = $this->SimpleArithmeticExpression();
2739
2740 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2741
2742 return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
2743 }
2744
2745 /**
2746 * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2747 */
2748 public function QuantifiedExpression(): AST\QuantifiedExpression
2749 {
2750 assert($this->lexer->lookahead !== null);
2751 $lookaheadType = $this->lexer->lookahead->type;
2752 $value = $this->lexer->lookahead->value;
2753
2754 if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) {
2755 $this->syntaxError('ALL, ANY or SOME');
2756 }
2757
2758 $this->match($lookaheadType);
2759 $this->match(TokenType::T_OPEN_PARENTHESIS);
2760
2761 $qExpr = new AST\QuantifiedExpression($this->Subselect());
2762 $qExpr->type = $value;
2763
2764 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2765
2766 return $qExpr;
2767 }
2768
2769 /**
2770 * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
2771 */
2772 public function BetweenExpression(): AST\BetweenExpression
2773 {
2774 $not = false;
2775 $arithExpr1 = $this->ArithmeticExpression();
2776
2777 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2778 $this->match(TokenType::T_NOT);
2779 $not = true;
2780 }
2781
2782 $this->match(TokenType::T_BETWEEN);
2783 $arithExpr2 = $this->ArithmeticExpression();
2784 $this->match(TokenType::T_AND);
2785 $arithExpr3 = $this->ArithmeticExpression();
2786
2787 return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not);
2788 }
2789
2790 /**
2791 * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
2792 */
2793 public function ComparisonExpression(): AST\ComparisonExpression
2794 {
2795 $this->lexer->glimpse();
2796
2797 $leftExpr = $this->ArithmeticExpression();
2798 $operator = $this->ComparisonOperator();
2799 $rightExpr = $this->isNextAllAnySome()
2800 ? $this->QuantifiedExpression()
2801 : $this->ArithmeticExpression();
2802
2803 return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
2804 }
2805
2806 /**
2807 * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
2808 */
2809 public function InExpression(): AST\InListExpression|AST\InSubselectExpression
2810 {
2811 $expression = $this->ArithmeticExpression();
2812
2813 $not = false;
2814 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2815 $this->match(TokenType::T_NOT);
2816 $not = true;
2817 }
2818
2819 $this->match(TokenType::T_IN);
2820 $this->match(TokenType::T_OPEN_PARENTHESIS);
2821
2822 if ($this->lexer->isNextToken(TokenType::T_SELECT)) {
2823 $inExpression = new AST\InSubselectExpression(
2824 $expression,
2825 $this->Subselect(),
2826 $not,
2827 );
2828 } else {
2829 $literals = [$this->InParameter()];
2830
2831 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
2832 $this->match(TokenType::T_COMMA);
2833 $literals[] = $this->InParameter();
2834 }
2835
2836 $inExpression = new AST\InListExpression(
2837 $expression,
2838 $literals,
2839 $not,
2840 );
2841 }
2842
2843 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2844
2845 return $inExpression;
2846 }
2847
2848 /**
2849 * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
2850 */
2851 public function InstanceOfExpression(): AST\InstanceOfExpression
2852 {
2853 $identificationVariable = $this->IdentificationVariable();
2854
2855 $not = false;
2856 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2857 $this->match(TokenType::T_NOT);
2858 $not = true;
2859 }
2860
2861 $this->match(TokenType::T_INSTANCE);
2862 $this->match(TokenType::T_OF);
2863
2864 $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)
2865 ? $this->InstanceOfParameterList()
2866 : [$this->InstanceOfParameter()];
2867
2868 return new AST\InstanceOfExpression(
2869 $identificationVariable,
2870 $exprValues,
2871 $not,
2872 );
2873 }
2874
2875 /** @return non-empty-list<AST\InputParameter|string> */
2876 public function InstanceOfParameterList(): array
2877 {
2878 $this->match(TokenType::T_OPEN_PARENTHESIS);
2879
2880 $exprValues = [$this->InstanceOfParameter()];
2881
2882 while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
2883 $this->match(TokenType::T_COMMA);
2884
2885 $exprValues[] = $this->InstanceOfParameter();
2886 }
2887
2888 $this->match(TokenType::T_CLOSE_PARENTHESIS);
2889
2890 return $exprValues;
2891 }
2892
2893 /**
2894 * InstanceOfParameter ::= AbstractSchemaName | InputParameter
2895 */
2896 public function InstanceOfParameter(): AST\InputParameter|string
2897 {
2898 if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
2899 $this->match(TokenType::T_INPUT_PARAMETER);
2900 assert($this->lexer->token !== null);
2901
2902 return new AST\InputParameter($this->lexer->token->value);
2903 }
2904
2905 $abstractSchemaName = $this->AbstractSchemaName();
2906
2907 $this->validateAbstractSchemaName($abstractSchemaName);
2908
2909 return $abstractSchemaName;
2910 }
2911
2912 /**
2913 * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
2914 */
2915 public function LikeExpression(): AST\LikeExpression
2916 {
2917 $stringExpr = $this->StringExpression();
2918 $not = false;
2919
2920 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
2921 $this->match(TokenType::T_NOT);
2922 $not = true;
2923 }
2924
2925 $this->match(TokenType::T_LIKE);
2926
2927 if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {
2928 $this->match(TokenType::T_INPUT_PARAMETER);
2929 assert($this->lexer->token !== null);
2930 $stringPattern = new AST\InputParameter($this->lexer->token->value);
2931 } else {
2932 $stringPattern = $this->StringPrimary();
2933 }
2934
2935 $escapeChar = null;
2936
2937 if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) {
2938 $this->match(TokenType::T_ESCAPE);
2939 $this->match(TokenType::T_STRING);
2940 assert($this->lexer->token !== null);
2941
2942 $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);
2943 }
2944
2945 return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not);
2946 }
2947
2948 /**
2949 * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
2950 */
2951 public function NullComparisonExpression(): AST\NullComparisonExpression
2952 {
2953 switch (true) {
2954 case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER):
2955 $this->match(TokenType::T_INPUT_PARAMETER);
2956 assert($this->lexer->token !== null);
2957
2958 $expr = new AST\InputParameter($this->lexer->token->value);
2959 break;
2960
2961 case $this->lexer->isNextToken(TokenType::T_NULLIF):
2962 $expr = $this->NullIfExpression();
2963 break;
2964
2965 case $this->lexer->isNextToken(TokenType::T_COALESCE):
2966 $expr = $this->CoalesceExpression();
2967 break;
2968
2969 case $this->isFunction():
2970 $expr = $this->FunctionDeclaration();
2971 break;
2972
2973 default:
2974 // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
2975 $glimpse = $this->lexer->glimpse();
2976 assert($glimpse !== null);
2977
2978 if ($glimpse->type === TokenType::T_DOT) {
2979 $expr = $this->SingleValuedPathExpression();
2980
2981 // Leave switch statement
2982 break;
2983 }
2984
2985 assert($this->lexer->lookahead !== null);
2986 $lookaheadValue = $this->lexer->lookahead->value;
2987
2988 // Validate existing component
2989 if (! isset($this->queryComponents[$lookaheadValue])) {
2990 $this->semanticalError('Cannot add having condition on undefined result variable.');
2991 }
2992
2993 // Validate SingleValuedPathExpression (ie.: "product")
2994 if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
2995 $expr = $this->SingleValuedPathExpression();
2996 break;
2997 }
2998
2999 // Validating ResultVariable
3000 if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3001 $this->semanticalError('Cannot add having condition on a non result variable.');
3002 }
3003
3004 $expr = $this->ResultVariable();
3005 break;
3006 }
3007
3008 $this->match(TokenType::T_IS);
3009
3010 $not = false;
3011 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
3012 $this->match(TokenType::T_NOT);
3013
3014 $not = true;
3015 }
3016
3017 $this->match(TokenType::T_NULL);
3018
3019 return new AST\NullComparisonExpression($expr, $not);
3020 }
3021
3022 /**
3023 * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3024 */
3025 public function ExistsExpression(): AST\ExistsExpression
3026 {
3027 $not = false;
3028
3029 if ($this->lexer->isNextToken(TokenType::T_NOT)) {
3030 $this->match(TokenType::T_NOT);
3031 $not = true;
3032 }
3033
3034 $this->match(TokenType::T_EXISTS);
3035 $this->match(TokenType::T_OPEN_PARENTHESIS);
3036
3037 $subselect = $this->Subselect();
3038
3039 $this->match(TokenType::T_CLOSE_PARENTHESIS);
3040
3041 return new AST\ExistsExpression($subselect, $not);
3042 }
3043
3044 /**
3045 * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3046 */
3047 public function ComparisonOperator(): string
3048 {
3049 assert($this->lexer->lookahead !== null);
3050 switch ($this->lexer->lookahead->value) {
3051 case '=':
3052 $this->match(TokenType::T_EQUALS);
3053
3054 return '=';
3055
3056 case '<':
3057 $this->match(TokenType::T_LOWER_THAN);
3058 $operator = '<';
3059
3060 if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {
3061 $this->match(TokenType::T_EQUALS);
3062 $operator .= '=';
3063 } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) {
3064 $this->match(TokenType::T_GREATER_THAN);
3065 $operator .= '>';
3066 }
3067
3068 return $operator;
3069
3070 case '>':
3071 $this->match(TokenType::T_GREATER_THAN);
3072 $operator = '>';
3073
3074 if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {
3075 $this->match(TokenType::T_EQUALS);
3076 $operator .= '=';
3077 }
3078
3079 return $operator;
3080
3081 case '!':
3082 $this->match(TokenType::T_NEGATE);
3083 $this->match(TokenType::T_EQUALS);
3084
3085 return '<>';
3086
3087 default:
3088 $this->syntaxError('=, <, <=, <>, >, >=, !=');
3089 }
3090 }
3091
3092 /**
3093 * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3094 */
3095 public function FunctionDeclaration(): Functions\FunctionNode
3096 {
3097 assert($this->lexer->lookahead !== null);
3098 $token = $this->lexer->lookahead;
3099 $funcName = strtolower($token->value);
3100
3101 $customFunctionDeclaration = $this->CustomFunctionDeclaration();
3102
3103 // Check for custom functions functions first!
3104 switch (true) {
3105 case $customFunctionDeclaration !== null:
3106 return $customFunctionDeclaration;
3107
3108 case isset(self::$stringFunctions[$funcName]):
3109 return $this->FunctionsReturningStrings();
3110
3111 case isset(self::$numericFunctions[$funcName]):
3112 return $this->FunctionsReturningNumerics();
3113
3114 case isset(self::$datetimeFunctions[$funcName]):
3115 return $this->FunctionsReturningDatetime();
3116
3117 default:
3118 $this->syntaxError('known function', $token);
3119 }
3120 }
3121
3122 /**
3123 * Helper function for FunctionDeclaration grammar rule.
3124 */
3125 private function CustomFunctionDeclaration(): Functions\FunctionNode|null
3126 {
3127 assert($this->lexer->lookahead !== null);
3128 $token = $this->lexer->lookahead;
3129 $funcName = strtolower($token->value);
3130
3131 // Check for custom functions afterwards
3132 $config = $this->em->getConfiguration();
3133
3134 return match (true) {
3135 $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(),
3136 $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(),
3137 $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(),
3138 default => null,
3139 };
3140 }
3141
3142 /**
3143 * FunctionsReturningNumerics ::=
3144 * "LENGTH" "(" StringPrimary ")" |
3145 * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3146 * "ABS" "(" SimpleArithmeticExpression ")" |
3147 * "SQRT" "(" SimpleArithmeticExpression ")" |
3148 * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3149 * "SIZE" "(" CollectionValuedPathExpression ")" |
3150 * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3151 * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3152 * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3153 */
3154 public function FunctionsReturningNumerics(): AST\Functions\FunctionNode
3155 {
3156 assert($this->lexer->lookahead !== null);
3157 $funcNameLower = strtolower($this->lexer->lookahead->value);
3158 $funcClass = self::$numericFunctions[$funcNameLower];
3159
3160 $function = new $funcClass($funcNameLower);
3161 $function->parse($this);
3162
3163 return $function;
3164 }
3165
3166 public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode
3167 {
3168 assert($this->lexer->lookahead !== null);
3169 // getCustomNumericFunction is case-insensitive
3170 $functionName = strtolower($this->lexer->lookahead->value);
3171 $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3172
3173 assert($functionClass !== null);
3174
3175 $function = is_string($functionClass)
3176 ? new $functionClass($functionName)
3177 : $functionClass($functionName);
3178
3179 $function->parse($this);
3180
3181 return $function;
3182 }
3183
3184 /**
3185 * FunctionsReturningDateTime ::=
3186 * "CURRENT_DATE" |
3187 * "CURRENT_TIME" |
3188 * "CURRENT_TIMESTAMP" |
3189 * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3190 * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3191 */
3192 public function FunctionsReturningDatetime(): AST\Functions\FunctionNode
3193 {
3194 assert($this->lexer->lookahead !== null);
3195 $funcNameLower = strtolower($this->lexer->lookahead->value);
3196 $funcClass = self::$datetimeFunctions[$funcNameLower];
3197
3198 $function = new $funcClass($funcNameLower);
3199 $function->parse($this);
3200
3201 return $function;
3202 }
3203
3204 public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode
3205 {
3206 assert($this->lexer->lookahead !== null);
3207 // getCustomDatetimeFunction is case-insensitive
3208 $functionName = $this->lexer->lookahead->value;
3209 $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3210
3211 assert($functionClass !== null);
3212
3213 $function = is_string($functionClass)
3214 ? new $functionClass($functionName)
3215 : $functionClass($functionName);
3216
3217 $function->parse($this);
3218
3219 return $function;
3220 }
3221
3222 /**
3223 * FunctionsReturningStrings ::=
3224 * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3225 * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3226 * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3227 * "LOWER" "(" StringPrimary ")" |
3228 * "UPPER" "(" StringPrimary ")" |
3229 * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3230 */
3231 public function FunctionsReturningStrings(): AST\Functions\FunctionNode
3232 {
3233 assert($this->lexer->lookahead !== null);
3234 $funcNameLower = strtolower($this->lexer->lookahead->value);
3235 $funcClass = self::$stringFunctions[$funcNameLower];
3236
3237 $function = new $funcClass($funcNameLower);
3238 $function->parse($this);
3239
3240 return $function;
3241 }
3242
3243 public function CustomFunctionsReturningStrings(): Functions\FunctionNode
3244 {
3245 assert($this->lexer->lookahead !== null);
3246 // getCustomStringFunction is case-insensitive
3247 $functionName = $this->lexer->lookahead->value;
3248 $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3249
3250 assert($functionClass !== null);
3251
3252 $function = is_string($functionClass)
3253 ? new $functionClass($functionName)
3254 : $functionClass($functionName);
3255
3256 $function->parse($this);
3257
3258 return $function;
3259 }
3260
3261 private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata
3262 {
3263 if (! isset($this->queryComponents[$dqlAlias]['metadata'])) {
3264 throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias));
3265 }
3266
3267 return $this->queryComponents[$dqlAlias]['metadata'];
3268 }
3269}
diff --git a/vendor/doctrine/orm/src/Query/ParserResult.php b/vendor/doctrine/orm/src/Query/ParserResult.php
new file mode 100644
index 0000000..8b5ee1f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/ParserResult.php
@@ -0,0 +1,118 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
8use LogicException;
9
10use function sprintf;
11
12/**
13 * Encapsulates the resulting components from a DQL query parsing process that
14 * can be serialized.
15 *
16 * @link http://www.doctrine-project.org
17 */
18class ParserResult
19{
20 /**
21 * The SQL executor used for executing the SQL.
22 */
23 private AbstractSqlExecutor|null $sqlExecutor = null;
24
25 /**
26 * The ResultSetMapping that describes how to map the SQL result set.
27 */
28 private ResultSetMapping $resultSetMapping;
29
30 /**
31 * The mappings of DQL parameter names/positions to SQL parameter positions.
32 *
33 * @psalm-var array<string|int, list<int>>
34 */
35 private array $parameterMappings = [];
36
37 /**
38 * Initializes a new instance of the <tt>ParserResult</tt> class.
39 * The new instance is initialized with an empty <tt>ResultSetMapping</tt>.
40 */
41 public function __construct()
42 {
43 $this->resultSetMapping = new ResultSetMapping();
44 }
45
46 /**
47 * Gets the ResultSetMapping for the parsed query.
48 *
49 * @return ResultSetMapping The result set mapping of the parsed query
50 */
51 public function getResultSetMapping(): ResultSetMapping
52 {
53 return $this->resultSetMapping;
54 }
55
56 /**
57 * Sets the ResultSetMapping of the parsed query.
58 */
59 public function setResultSetMapping(ResultSetMapping $rsm): void
60 {
61 $this->resultSetMapping = $rsm;
62 }
63
64 /**
65 * Sets the SQL executor that should be used for this ParserResult.
66 */
67 public function setSqlExecutor(AbstractSqlExecutor $executor): void
68 {
69 $this->sqlExecutor = $executor;
70 }
71
72 /**
73 * Gets the SQL executor used by this ParserResult.
74 */
75 public function getSqlExecutor(): AbstractSqlExecutor
76 {
77 if ($this->sqlExecutor === null) {
78 throw new LogicException(sprintf(
79 'Executor not set yet. Call %s::setSqlExecutor() first.',
80 self::class,
81 ));
82 }
83
84 return $this->sqlExecutor;
85 }
86
87 /**
88 * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to
89 * several SQL parameter positions.
90 */
91 public function addParameterMapping(string|int $dqlPosition, int $sqlPosition): void
92 {
93 $this->parameterMappings[$dqlPosition][] = $sqlPosition;
94 }
95
96 /**
97 * Gets all DQL to SQL parameter mappings.
98 *
99 * @psalm-return array<int|string, list<int>> The parameter mappings.
100 */
101 public function getParameterMappings(): array
102 {
103 return $this->parameterMappings;
104 }
105
106 /**
107 * Gets the SQL parameter positions for a DQL parameter name/position.
108 *
109 * @param string|int $dqlPosition The name or position of the DQL parameter.
110 *
111 * @return int[] The positions of the corresponding SQL parameters.
112 * @psalm-return list<int>
113 */
114 public function getSqlParameterPositions(string|int $dqlPosition): array
115 {
116 return $this->parameterMappings[$dqlPosition];
117 }
118}
diff --git a/vendor/doctrine/orm/src/Query/Printer.php b/vendor/doctrine/orm/src/Query/Printer.php
new file mode 100644
index 0000000..db1f159
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/Printer.php
@@ -0,0 +1,64 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use function str_repeat;
8
9/**
10 * A parse tree printer for Doctrine Query Language parser.
11 *
12 * @link http://www.phpdoctrine.org
13 */
14class Printer
15{
16 /** Current indentation level */
17 protected int $indent = 0;
18
19 /**
20 * Constructs a new parse tree printer.
21 *
22 * @param bool $silent Parse tree will not be printed if true.
23 */
24 public function __construct(protected bool $silent = false)
25 {
26 }
27
28 /**
29 * Prints an opening parenthesis followed by production name and increases
30 * indentation level by one.
31 *
32 * This method is called before executing a production.
33 *
34 * @param string $name Production name.
35 */
36 public function startProduction(string $name): void
37 {
38 $this->println('(' . $name);
39 $this->indent++;
40 }
41
42 /**
43 * Decreases indentation level by one and prints a closing parenthesis.
44 *
45 * This method is called after executing a production.
46 */
47 public function endProduction(): void
48 {
49 $this->indent--;
50 $this->println(')');
51 }
52
53 /**
54 * Prints text indented with spaces depending on current indentation level.
55 *
56 * @param string $str The text.
57 */
58 public function println(string $str): void
59 {
60 if (! $this->silent) {
61 echo str_repeat(' ', $this->indent), $str, "\n";
62 }
63 }
64}
diff --git a/vendor/doctrine/orm/src/Query/QueryException.php b/vendor/doctrine/orm/src/Query/QueryException.php
new file mode 100644
index 0000000..ae945b1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/QueryException.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\Exception\ORMException;
8use Doctrine\ORM\Mapping\AssociationMapping;
9use Doctrine\ORM\Query\AST\PathExpression;
10use Exception;
11use Stringable;
12use Throwable;
13
14class QueryException extends Exception implements ORMException
15{
16 public static function dqlError(string $dql): self
17 {
18 return new self($dql);
19 }
20
21 public static function syntaxError(string $message, Throwable|null $previous = null): self
22 {
23 return new self('[Syntax Error] ' . $message, 0, $previous);
24 }
25
26 public static function semanticalError(string $message, Throwable|null $previous = null): self
27 {
28 return new self('[Semantical Error] ' . $message, 0, $previous);
29 }
30
31 public static function invalidLockMode(): self
32 {
33 return new self('Invalid lock mode hint provided.');
34 }
35
36 public static function invalidParameterType(string $expected, string $received): self
37 {
38 return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.');
39 }
40
41 public static function invalidParameterPosition(string $pos): self
42 {
43 return new self('Invalid parameter position: ' . $pos);
44 }
45
46 public static function tooManyParameters(int $expected, int $received): self
47 {
48 return new self('Too many parameters: the query defines ' . $expected . ' parameters and you bound ' . $received);
49 }
50
51 public static function tooFewParameters(int $expected, int $received): self
52 {
53 return new self('Too few parameters: the query defines ' . $expected . ' parameters but you only bound ' . $received);
54 }
55
56 public static function invalidParameterFormat(string $value): self
57 {
58 return new self('Invalid parameter format, ' . $value . ' given, but :<name> or ?<num> expected.');
59 }
60
61 public static function unknownParameter(string $key): self
62 {
63 return new self('Invalid parameter: token ' . $key . ' is not defined in the query.');
64 }
65
66 public static function parameterTypeMismatch(): self
67 {
68 return new self('DQL Query parameter and type numbers mismatch, but have to be exactly equal.');
69 }
70
71 public static function invalidPathExpression(PathExpression $pathExpr): self
72 {
73 return new self(
74 "Invalid PathExpression '" . $pathExpr->identificationVariable . '.' . $pathExpr->field . "'.",
75 );
76 }
77
78 public static function invalidLiteral(string|Stringable $literal): self
79 {
80 return new self("Invalid literal '" . $literal . "'");
81 }
82
83 public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMapping $assoc): self
84 {
85 return new self(
86 'Invalid query operation: Not allowed to iterate over fetch join collections ' .
87 'in class ' . $assoc->sourceEntity . ' association ' . $assoc->fieldName,
88 );
89 }
90
91 /**
92 * @param string[] $assoc
93 * @psalm-param array<string, string> $assoc
94 */
95 public static function overwritingJoinConditionsNotYetSupported(array $assoc): self
96 {
97 return new self(
98 'Unsupported query operation: It is not yet possible to overwrite the join ' .
99 'conditions in class ' . $assoc['sourceEntityName'] . ' association ' . $assoc['fieldName'] . '. ' .
100 'Use WITH to append additional join conditions to the association.',
101 );
102 }
103
104 public static function associationPathInverseSideNotSupported(PathExpression $pathExpr): self
105 {
106 return new self(
107 'A single-valued association path expression to an inverse side is not supported in DQL queries. ' .
108 'Instead of "' . $pathExpr->identificationVariable . '.' . $pathExpr->field . '" use an explicit join.',
109 );
110 }
111
112 public static function iterateWithFetchJoinNotAllowed(AssociationMapping $assoc): self
113 {
114 return new self(
115 'Iterate with fetch join in class ' . $assoc->sourceEntity .
116 ' using association ' . $assoc->fieldName . ' not allowed.',
117 );
118 }
119
120 public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): self
121 {
122 return new self(
123 'Associations with fetch-mode=EAGER may not be using WITH conditions in
124 "' . $sourceEntity . '#' . $fieldName . '".',
125 );
126 }
127
128 public static function iterateWithMixedResultNotAllowed(): self
129 {
130 return new self('Iterating a query with mixed results (using scalars) is not supported.');
131 }
132
133 public static function associationPathCompositeKeyNotSupported(): self
134 {
135 return new self(
136 'A single-valued association path expression to an entity with a composite primary ' .
137 'key is not supported. Explicitly name the components of the composite primary key ' .
138 'in the query.',
139 );
140 }
141
142 public static function instanceOfUnrelatedClass(string $className, string $rootClass): self
143 {
144 return new self("Cannot check if a child of '" . $rootClass . "' is instanceof '" . $className . "', " .
145 'inheritance hierarchy does not exists between these two classes.');
146 }
147
148 public static function invalidQueryComponent(string $dqlAlias): self
149 {
150 return new self(
151 "Invalid query component given for DQL alias '" . $dqlAlias . "', " .
152 "requires 'metadata', 'parent', 'relation', 'map', 'nestingLevel' and 'token' keys.",
153 );
154 }
155}
diff --git a/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php
new file mode 100644
index 0000000..3e0ec65
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php
@@ -0,0 +1,180 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\Common\Collections\ArrayCollection;
8use Doctrine\Common\Collections\Expr\Comparison;
9use Doctrine\Common\Collections\Expr\CompositeExpression;
10use Doctrine\Common\Collections\Expr\ExpressionVisitor;
11use Doctrine\Common\Collections\Expr\Value;
12use RuntimeException;
13
14use function count;
15use function str_replace;
16use function str_starts_with;
17
18/**
19 * Converts Collection expressions to Query expressions.
20 */
21class QueryExpressionVisitor extends ExpressionVisitor
22{
23 private const OPERATOR_MAP = [
24 Comparison::GT => Expr\Comparison::GT,
25 Comparison::GTE => Expr\Comparison::GTE,
26 Comparison::LT => Expr\Comparison::LT,
27 Comparison::LTE => Expr\Comparison::LTE,
28 ];
29
30 private readonly Expr $expr;
31
32 /** @var list<mixed> */
33 private array $parameters = [];
34
35 /** @param mixed[] $queryAliases */
36 public function __construct(
37 private readonly array $queryAliases,
38 ) {
39 $this->expr = new Expr();
40 }
41
42 /**
43 * Gets bound parameters.
44 * Filled after {@link dispach()}.
45 *
46 * @return ArrayCollection<int, mixed>
47 */
48 public function getParameters(): ArrayCollection
49 {
50 return new ArrayCollection($this->parameters);
51 }
52
53 public function clearParameters(): void
54 {
55 $this->parameters = [];
56 }
57
58 /**
59 * Converts Criteria expression to Query one based on static map.
60 */
61 private static function convertComparisonOperator(string $criteriaOperator): string|null
62 {
63 return self::OPERATOR_MAP[$criteriaOperator] ?? null;
64 }
65
66 public function walkCompositeExpression(CompositeExpression $expr): mixed
67 {
68 $expressionList = [];
69
70 foreach ($expr->getExpressionList() as $child) {
71 $expressionList[] = $this->dispatch($child);
72 }
73
74 return match ($expr->getType()) {
75 CompositeExpression::TYPE_AND => new Expr\Andx($expressionList),
76 CompositeExpression::TYPE_OR => new Expr\Orx($expressionList),
77 CompositeExpression::TYPE_NOT => $this->expr->not($expressionList[0]),
78 default => throw new RuntimeException('Unknown composite ' . $expr->getType()),
79 };
80 }
81
82 public function walkComparison(Comparison $comparison): mixed
83 {
84 if (! isset($this->queryAliases[0])) {
85 throw new QueryException('No aliases are set before invoking walkComparison().');
86 }
87
88 $field = $this->queryAliases[0] . '.' . $comparison->getField();
89
90 foreach ($this->queryAliases as $alias) {
91 if (str_starts_with($comparison->getField() . '.', $alias . '.')) {
92 $field = $comparison->getField();
93 break;
94 }
95 }
96
97 $parameterName = str_replace('.', '_', $comparison->getField());
98
99 foreach ($this->parameters as $parameter) {
100 if ($parameter->getName() === $parameterName) {
101 $parameterName .= '_' . count($this->parameters);
102 break;
103 }
104 }
105
106 $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue()));
107 $placeholder = ':' . $parameterName;
108
109 switch ($comparison->getOperator()) {
110 case Comparison::IN:
111 $this->parameters[] = $parameter;
112
113 return $this->expr->in($field, $placeholder);
114
115 case Comparison::NIN:
116 $this->parameters[] = $parameter;
117
118 return $this->expr->notIn($field, $placeholder);
119
120 case Comparison::EQ:
121 case Comparison::IS:
122 if ($this->walkValue($comparison->getValue()) === null) {
123 return $this->expr->isNull($field);
124 }
125
126 $this->parameters[] = $parameter;
127
128 return $this->expr->eq($field, $placeholder);
129
130 case Comparison::NEQ:
131 if ($this->walkValue($comparison->getValue()) === null) {
132 return $this->expr->isNotNull($field);
133 }
134
135 $this->parameters[] = $parameter;
136
137 return $this->expr->neq($field, $placeholder);
138
139 case Comparison::CONTAINS:
140 $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType());
141 $this->parameters[] = $parameter;
142
143 return $this->expr->like($field, $placeholder);
144
145 case Comparison::MEMBER_OF:
146 return $this->expr->isMemberOf($comparison->getField(), $comparison->getValue()->getValue());
147
148 case Comparison::STARTS_WITH:
149 $parameter->setValue($parameter->getValue() . '%', $parameter->getType());
150 $this->parameters[] = $parameter;
151
152 return $this->expr->like($field, $placeholder);
153
154 case Comparison::ENDS_WITH:
155 $parameter->setValue('%' . $parameter->getValue(), $parameter->getType());
156 $this->parameters[] = $parameter;
157
158 return $this->expr->like($field, $placeholder);
159
160 default:
161 $operator = self::convertComparisonOperator($comparison->getOperator());
162 if ($operator) {
163 $this->parameters[] = $parameter;
164
165 return new Expr\Comparison(
166 $field,
167 $operator,
168 $placeholder,
169 );
170 }
171
172 throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator());
173 }
174 }
175
176 public function walkValue(Value $value): mixed
177 {
178 return $value->getValue();
179 }
180}
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMapping.php b/vendor/doctrine/orm/src/Query/ResultSetMapping.php
new file mode 100644
index 0000000..612474d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/ResultSetMapping.php
@@ -0,0 +1,547 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use function count;
8
9/**
10 * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result.
11 *
12 * IMPORTANT NOTE:
13 * The properties of this class are only public for fast internal READ access and to (drastically)
14 * reduce the size of serialized instances for more effective caching due to better (un-)serialization
15 * performance.
16 *
17 * <b>Users should use the public methods.</b>
18 *
19 * @todo Think about whether the number of lookup maps can be reduced.
20 */
21class ResultSetMapping
22{
23 /**
24 * Whether the result is mixed (contains scalar values together with field values).
25 *
26 * @ignore
27 */
28 public bool $isMixed = false;
29
30 /**
31 * Whether the result is a select statement.
32 *
33 * @ignore
34 */
35 public bool $isSelect = true;
36
37 /**
38 * Maps alias names to class names.
39 *
40 * @ignore
41 * @psalm-var array<string, class-string>
42 */
43 public array $aliasMap = [];
44
45 /**
46 * Maps alias names to related association field names.
47 *
48 * @ignore
49 * @psalm-var array<string, string>
50 */
51 public array $relationMap = [];
52
53 /**
54 * Maps alias names to parent alias names.
55 *
56 * @ignore
57 * @psalm-var array<string, string>
58 */
59 public array $parentAliasMap = [];
60
61 /**
62 * Maps column names in the result set to field names for each class.
63 *
64 * @ignore
65 * @psalm-var array<string, string>
66 */
67 public array $fieldMappings = [];
68
69 /**
70 * Maps column names in the result set to the alias/field name to use in the mapped result.
71 *
72 * @ignore
73 * @psalm-var array<string, string|int>
74 */
75 public array $scalarMappings = [];
76
77 /**
78 * Maps scalar columns to enums
79 *
80 * @ignore
81 * @psalm-var array<string, string>
82 */
83 public $enumMappings = [];
84
85 /**
86 * Maps column names in the result set to the alias/field type to use in the mapped result.
87 *
88 * @ignore
89 * @psalm-var array<string, string>
90 */
91 public array $typeMappings = [];
92
93 /**
94 * Maps entities in the result set to the alias name to use in the mapped result.
95 *
96 * @ignore
97 * @psalm-var array<string, string|null>
98 */
99 public array $entityMappings = [];
100
101 /**
102 * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names.
103 *
104 * @ignore
105 * @psalm-var array<string, string>
106 */
107 public array $metaMappings = [];
108
109 /**
110 * Maps column names in the result set to the alias they belong to.
111 *
112 * @ignore
113 * @psalm-var array<string, string>
114 */
115 public array $columnOwnerMap = [];
116
117 /**
118 * List of columns in the result set that are used as discriminator columns.
119 *
120 * @ignore
121 * @psalm-var array<string, string>
122 */
123 public array $discriminatorColumns = [];
124
125 /**
126 * Maps alias names to field names that should be used for indexing.
127 *
128 * @ignore
129 * @psalm-var array<string, string>
130 */
131 public array $indexByMap = [];
132
133 /**
134 * Map from column names to class names that declare the field the column is mapped to.
135 *
136 * @ignore
137 * @psalm-var array<string, class-string>
138 */
139 public array $declaringClasses = [];
140
141 /**
142 * This is necessary to hydrate derivate foreign keys correctly.
143 *
144 * @psalm-var array<string, array<string, bool>>
145 */
146 public array $isIdentifierColumn = [];
147
148 /**
149 * Maps column names in the result set to field names for each new object expression.
150 *
151 * @psalm-var array<string, array<string, mixed>>
152 */
153 public array $newObjectMappings = [];
154
155 /**
156 * Maps metadata parameter names to the metadata attribute.
157 *
158 * @psalm-var array<int|string, string>
159 */
160 public array $metadataParameterMapping = [];
161
162 /**
163 * Contains query parameter names to be resolved as discriminator values
164 *
165 * @psalm-var array<string, string>
166 */
167 public array $discriminatorParameters = [];
168
169 /**
170 * Adds an entity result to this ResultSetMapping.
171 *
172 * @param string $class The class name of the entity.
173 * @param string $alias The alias for the class. The alias must be unique among all entity
174 * results or joined entity results within this ResultSetMapping.
175 * @param string|null $resultAlias The result alias with which the entity result should be
176 * placed in the result structure.
177 * @psalm-param class-string $class
178 *
179 * @return $this
180 *
181 * @todo Rename: addRootEntity
182 */
183 public function addEntityResult(string $class, string $alias, string|null $resultAlias = null): static
184 {
185 $this->aliasMap[$alias] = $class;
186 $this->entityMappings[$alias] = $resultAlias;
187
188 if ($resultAlias !== null) {
189 $this->isMixed = true;
190 }
191
192 return $this;
193 }
194
195 /**
196 * Sets a discriminator column for an entity result or joined entity result.
197 * The discriminator column will be used to determine the concrete class name to
198 * instantiate.
199 *
200 * @param string $alias The alias of the entity result or joined entity result the discriminator
201 * column should be used for.
202 * @param string $discrColumn The name of the discriminator column in the SQL result set.
203 *
204 * @return $this
205 *
206 * @todo Rename: addDiscriminatorColumn
207 */
208 public function setDiscriminatorColumn(string $alias, string $discrColumn): static
209 {
210 $this->discriminatorColumns[$alias] = $discrColumn;
211 $this->columnOwnerMap[$discrColumn] = $alias;
212
213 return $this;
214 }
215
216 /**
217 * Sets a field to use for indexing an entity result or joined entity result.
218 *
219 * @param string $alias The alias of an entity result or joined entity result.
220 * @param string $fieldName The name of the field to use for indexing.
221 *
222 * @return $this
223 */
224 public function addIndexBy(string $alias, string $fieldName): static
225 {
226 $found = false;
227
228 foreach ([...$this->metaMappings, ...$this->fieldMappings] as $columnName => $columnFieldName) {
229 if (! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) {
230 continue;
231 }
232
233 $this->addIndexByColumn($alias, $columnName);
234 $found = true;
235
236 break;
237 }
238
239 /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals
240 if ( ! $found) {
241 $message = sprintf(
242 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.',
243 $alias,
244 $fieldName
245 );
246
247 throw new \LogicException($message);
248 }
249 */
250
251 return $this;
252 }
253
254 /**
255 * Sets to index by a scalar result column name.
256 *
257 * @return $this
258 */
259 public function addIndexByScalar(string $resultColumnName): static
260 {
261 $this->indexByMap['scalars'] = $resultColumnName;
262
263 return $this;
264 }
265
266 /**
267 * Sets a column to use for indexing an entity or joined entity result by the given alias name.
268 *
269 * @return $this
270 */
271 public function addIndexByColumn(string $alias, string $resultColumnName): static
272 {
273 $this->indexByMap[$alias] = $resultColumnName;
274
275 return $this;
276 }
277
278 /**
279 * Checks whether an entity result or joined entity result with a given alias has
280 * a field set for indexing.
281 *
282 * @todo Rename: isIndexed($alias)
283 */
284 public function hasIndexBy(string $alias): bool
285 {
286 return isset($this->indexByMap[$alias]);
287 }
288
289 /**
290 * Checks whether the column with the given name is mapped as a field result
291 * as part of an entity result or joined entity result.
292 *
293 * @param string $columnName The name of the column in the SQL result set.
294 *
295 * @todo Rename: isField
296 */
297 public function isFieldResult(string $columnName): bool
298 {
299 return isset($this->fieldMappings[$columnName]);
300 }
301
302 /**
303 * Adds a field to the result that belongs to an entity or joined entity.
304 *
305 * @param string $alias The alias of the root entity or joined entity to which the field belongs.
306 * @param string $columnName The name of the column in the SQL result set.
307 * @param string $fieldName The name of the field on the declaring class.
308 * @param string|null $declaringClass The name of the class that declares/owns the specified field.
309 * When $alias refers to a superclass in a mapped hierarchy but
310 * the field $fieldName is defined on a subclass, specify that here.
311 * If not specified, the field is assumed to belong to the class
312 * designated by $alias.
313 * @psalm-param class-string|null $declaringClass
314 *
315 * @return $this
316 *
317 * @todo Rename: addField
318 */
319 public function addFieldResult(string $alias, string $columnName, string $fieldName, string|null $declaringClass = null): static
320 {
321 // column name (in result set) => field name
322 $this->fieldMappings[$columnName] = $fieldName;
323 // column name => alias of owner
324 $this->columnOwnerMap[$columnName] = $alias;
325 // field name => class name of declaring class
326 $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias];
327
328 if (! $this->isMixed && $this->scalarMappings) {
329 $this->isMixed = true;
330 }
331
332 return $this;
333 }
334
335 /**
336 * Adds a joined entity result.
337 *
338 * @param string $class The class name of the joined entity.
339 * @param string $alias The unique alias to use for the joined entity.
340 * @param string $parentAlias The alias of the entity result that is the parent of this joined result.
341 * @param string $relation The association field that connects the parent entity result
342 * with the joined entity result.
343 * @psalm-param class-string $class
344 *
345 * @return $this
346 *
347 * @todo Rename: addJoinedEntity
348 */
349 public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation): static
350 {
351 $this->aliasMap[$alias] = $class;
352 $this->parentAliasMap[$alias] = $parentAlias;
353 $this->relationMap[$alias] = $relation;
354
355 return $this;
356 }
357
358 /**
359 * Adds a scalar result mapping.
360 *
361 * @param string $columnName The name of the column in the SQL result set.
362 * @param string|int $alias The result alias with which the scalar result should be placed in the result structure.
363 * @param string $type The column type
364 *
365 * @return $this
366 *
367 * @todo Rename: addScalar
368 */
369 public function addScalarResult(string $columnName, string|int $alias, string $type = 'string'): static
370 {
371 $this->scalarMappings[$columnName] = $alias;
372 $this->typeMappings[$columnName] = $type;
373
374 if (! $this->isMixed && $this->fieldMappings) {
375 $this->isMixed = true;
376 }
377
378 return $this;
379 }
380
381 /**
382 * Adds a scalar result mapping.
383 *
384 * @param string $columnName The name of the column in the SQL result set.
385 * @param string $enumType The enum type
386 *
387 * @return $this
388 */
389 public function addEnumResult(string $columnName, string $enumType): static
390 {
391 $this->enumMappings[$columnName] = $enumType;
392
393 return $this;
394 }
395
396 /**
397 * Adds a metadata parameter mappings.
398 */
399 public function addMetadataParameterMapping(string|int $parameter, string $attribute): void
400 {
401 $this->metadataParameterMapping[$parameter] = $attribute;
402 }
403
404 /**
405 * Checks whether a column with a given name is mapped as a scalar result.
406 *
407 * @todo Rename: isScalar
408 */
409 public function isScalarResult(string $columnName): bool
410 {
411 return isset($this->scalarMappings[$columnName]);
412 }
413
414 /**
415 * Gets the name of the class of an entity result or joined entity result,
416 * identified by the given unique alias.
417 *
418 * @psalm-return class-string
419 */
420 public function getClassName(string $alias): string
421 {
422 return $this->aliasMap[$alias];
423 }
424
425 /**
426 * Gets the field alias for a column that is mapped as a scalar value.
427 *
428 * @param string $columnName The name of the column in the SQL result set.
429 */
430 public function getScalarAlias(string $columnName): string|int
431 {
432 return $this->scalarMappings[$columnName];
433 }
434
435 /**
436 * Gets the name of the class that owns a field mapping for the specified column.
437 *
438 * @psalm-return class-string
439 */
440 public function getDeclaringClass(string $columnName): string
441 {
442 return $this->declaringClasses[$columnName];
443 }
444
445 public function getRelation(string $alias): string
446 {
447 return $this->relationMap[$alias];
448 }
449
450 public function isRelation(string $alias): bool
451 {
452 return isset($this->relationMap[$alias]);
453 }
454
455 /**
456 * Gets the alias of the class that owns a field mapping for the specified column.
457 */
458 public function getEntityAlias(string $columnName): string
459 {
460 return $this->columnOwnerMap[$columnName];
461 }
462
463 /**
464 * Gets the parent alias of the given alias.
465 */
466 public function getParentAlias(string $alias): string
467 {
468 return $this->parentAliasMap[$alias];
469 }
470
471 /**
472 * Checks whether the given alias has a parent alias.
473 */
474 public function hasParentAlias(string $alias): bool
475 {
476 return isset($this->parentAliasMap[$alias]);
477 }
478
479 /**
480 * Gets the field name for a column name.
481 */
482 public function getFieldName(string $columnName): string
483 {
484 return $this->fieldMappings[$columnName];
485 }
486
487 /** @psalm-return array<string, class-string> */
488 public function getAliasMap(): array
489 {
490 return $this->aliasMap;
491 }
492
493 /**
494 * Gets the number of different entities that appear in the mapped result.
495 *
496 * @psalm-return 0|positive-int
497 */
498 public function getEntityResultCount(): int
499 {
500 return count($this->aliasMap);
501 }
502
503 /**
504 * Checks whether this ResultSetMapping defines a mixed result.
505 *
506 * Mixed results can only occur in object and array (graph) hydration. In such a
507 * case a mixed result means that scalar values are mixed with objects/array in
508 * the result.
509 */
510 public function isMixedResult(): bool
511 {
512 return $this->isMixed;
513 }
514
515 /**
516 * Adds a meta column (foreign key or discriminator column) to the result set.
517 *
518 * @param string $alias The result alias with which the meta result should be placed in the result structure.
519 * @param string $columnName The name of the column in the SQL result set.
520 * @param string $fieldName The name of the field on the declaring class.
521 * @param string|null $type The column type
522 *
523 * @return $this
524 *
525 * @todo Make all methods of this class require all parameters and not infer anything
526 */
527 public function addMetaResult(
528 string $alias,
529 string $columnName,
530 string $fieldName,
531 bool $isIdentifierColumn = false,
532 string|null $type = null,
533 ): static {
534 $this->metaMappings[$columnName] = $fieldName;
535 $this->columnOwnerMap[$columnName] = $alias;
536
537 if ($isIdentifierColumn) {
538 $this->isIdentifierColumn[$alias][$columnName] = true;
539 }
540
541 if ($type) {
542 $this->typeMappings[$columnName] = $type;
543 }
544
545 return $this;
546 }
547}
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php
new file mode 100644
index 0000000..f28f3a9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php
@@ -0,0 +1,281 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\ORM\EntityManagerInterface;
9use Doctrine\ORM\Internal\SQLResultCasing;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Utility\PersisterHelper;
12use InvalidArgumentException;
13use Stringable;
14
15use function in_array;
16use function sprintf;
17
18/**
19 * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields.
20 */
21class ResultSetMappingBuilder extends ResultSetMapping implements Stringable
22{
23 use SQLResultCasing;
24
25 /**
26 * Picking this rename mode will register entity columns as is,
27 * as they are in the database. This can cause clashes when multiple
28 * entities are fetched that have columns with the same name.
29 */
30 public const COLUMN_RENAMING_NONE = 1;
31
32 /**
33 * Picking custom renaming allows the user to define the renaming
34 * of specific columns with a rename array that contains column names as
35 * keys and result alias as values.
36 */
37 public const COLUMN_RENAMING_CUSTOM = 2;
38
39 /**
40 * Incremental renaming uses a result set mapping internal counter to add a
41 * number to each column result, leading to uniqueness. This only works if
42 * you use {@see generateSelectClause()} to generate the SELECT clause for
43 * you.
44 */
45 public const COLUMN_RENAMING_INCREMENT = 3;
46
47 private int $sqlCounter = 0;
48
49 /** @psalm-param self::COLUMN_RENAMING_* $defaultRenameMode */
50 public function __construct(
51 private readonly EntityManagerInterface $em,
52 private readonly int $defaultRenameMode = self::COLUMN_RENAMING_NONE,
53 ) {
54 }
55
56 /**
57 * Adds a root entity and all of its fields to the result set.
58 *
59 * @param string $class The class name of the root entity.
60 * @param string $alias The unique alias to use for the root entity.
61 * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
62 * @psalm-param class-string $class
63 * @psalm-param array<string, string> $renamedColumns
64 * @psalm-param self::COLUMN_RENAMING_*|null $renameMode
65 */
66 public function addRootEntityFromClassMetadata(
67 string $class,
68 string $alias,
69 array $renamedColumns = [],
70 int|null $renameMode = null,
71 ): void {
72 $renameMode = $renameMode ?: $this->defaultRenameMode;
73 $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
74
75 $this->addEntityResult($class, $alias);
76 $this->addAllClassFields($class, $alias, $columnAliasMap);
77 }
78
79 /**
80 * Adds a joined entity and all of its fields to the result set.
81 *
82 * @param string $class The class name of the joined entity.
83 * @param string $alias The unique alias to use for the joined entity.
84 * @param string $parentAlias The alias of the entity result that is the parent of this joined result.
85 * @param string $relation The association field that connects the parent entity result
86 * with the joined entity result.
87 * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
88 * @psalm-param class-string $class
89 * @psalm-param array<string, string> $renamedColumns
90 * @psalm-param self::COLUMN_RENAMING_*|null $renameMode
91 */
92 public function addJoinedEntityFromClassMetadata(
93 string $class,
94 string $alias,
95 string $parentAlias,
96 string $relation,
97 array $renamedColumns = [],
98 int|null $renameMode = null,
99 ): void {
100 $renameMode = $renameMode ?: $this->defaultRenameMode;
101 $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
102
103 $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation);
104 $this->addAllClassFields($class, $alias, $columnAliasMap);
105 }
106
107 /**
108 * Adds all fields of the given class to the result set mapping (columns and meta fields).
109 *
110 * @param string[] $columnAliasMap
111 * @psalm-param array<string, string> $columnAliasMap
112 *
113 * @throws InvalidArgumentException
114 */
115 protected function addAllClassFields(string $class, string $alias, array $columnAliasMap = []): void
116 {
117 $classMetadata = $this->em->getClassMetadata($class);
118 $platform = $this->em->getConnection()->getDatabasePlatform();
119
120 if (! $this->isInheritanceSupported($classMetadata)) {
121 throw new InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.');
122 }
123
124 foreach ($classMetadata->getColumnNames() as $columnName) {
125 $propertyName = $classMetadata->getFieldName($columnName);
126 $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]);
127
128 if (isset($this->fieldMappings[$columnAlias])) {
129 throw new InvalidArgumentException(sprintf(
130 "The column '%s' conflicts with another column in the mapper.",
131 $columnName,
132 ));
133 }
134
135 $this->addFieldResult($alias, $columnAlias, $propertyName);
136
137 $enumType = $classMetadata->getFieldMapping($propertyName)->enumType ?? null;
138 if (! empty($enumType)) {
139 $this->addEnumResult($columnAlias, $enumType);
140 }
141 }
142
143 foreach ($classMetadata->associationMappings as $associationMapping) {
144 if ($associationMapping->isToOneOwningSide()) {
145 $targetClass = $this->em->getClassMetadata($associationMapping->targetEntity);
146 $isIdentifier = isset($associationMapping->id) && $associationMapping->id === true;
147
148 foreach ($associationMapping->joinColumns as $joinColumn) {
149 $columnName = $joinColumn->name;
150 $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]);
151 $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
152
153 if (isset($this->metaMappings[$columnAlias])) {
154 throw new InvalidArgumentException(sprintf(
155 "The column '%s' conflicts with another column in the mapper.",
156 $columnAlias,
157 ));
158 }
159
160 $this->addMetaResult($alias, $columnAlias, $columnName, $isIdentifier, $columnType);
161 }
162 }
163 }
164 }
165
166 private function isInheritanceSupported(ClassMetadata $classMetadata): bool
167 {
168 if (
169 $classMetadata->isInheritanceTypeSingleTable()
170 && in_array($classMetadata->name, $classMetadata->discriminatorMap, true)
171 ) {
172 return true;
173 }
174
175 return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined());
176 }
177
178 /**
179 * Gets column alias for a given column.
180 *
181 * @psalm-param array<string, string> $customRenameColumns
182 *
183 * @psalm-assert self::COLUMN_RENAMING_* $mode
184 */
185 private function getColumnAlias(string $columnName, int $mode, array $customRenameColumns): string
186 {
187 return match ($mode) {
188 self::COLUMN_RENAMING_INCREMENT => $columnName . $this->sqlCounter++,
189 self::COLUMN_RENAMING_CUSTOM => $customRenameColumns[$columnName] ?? $columnName,
190 self::COLUMN_RENAMING_NONE => $columnName,
191 default => throw new InvalidArgumentException(sprintf('%d is not a valid value for $mode', $mode)),
192 };
193 }
194
195 /**
196 * Retrieves a class columns and join columns aliases that are used in the SELECT clause.
197 *
198 * This depends on the renaming mode selected by the user.
199 *
200 * @psalm-param class-string $className
201 * @psalm-param self::COLUMN_RENAMING_* $mode
202 * @psalm-param array<string, string> $customRenameColumns
203 *
204 * @return string[]
205 * @psalm-return array<array-key, string>
206 */
207 private function getColumnAliasMap(
208 string $className,
209 int $mode,
210 array $customRenameColumns,
211 ): array {
212 if ($customRenameColumns) { // for BC with 2.2-2.3 API
213 $mode = self::COLUMN_RENAMING_CUSTOM;
214 }
215
216 $columnAlias = [];
217 $class = $this->em->getClassMetadata($className);
218
219 foreach ($class->getColumnNames() as $columnName) {
220 $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
221 }
222
223 foreach ($class->associationMappings as $associationMapping) {
224 if ($associationMapping->isToOneOwningSide()) {
225 foreach ($associationMapping->joinColumns as $joinColumn) {
226 $columnName = $joinColumn->name;
227 $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
228 }
229 }
230 }
231
232 return $columnAlias;
233 }
234
235 /**
236 * Generates the Select clause from this ResultSetMappingBuilder.
237 *
238 * Works only for all the entity results. The select parts for scalar
239 * expressions have to be written manually.
240 *
241 * @param string[] $tableAliases
242 * @psalm-param array<string, string> $tableAliases
243 */
244 public function generateSelectClause(array $tableAliases = []): string
245 {
246 $sql = '';
247
248 foreach ($this->columnOwnerMap as $columnName => $dqlAlias) {
249 $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias;
250
251 if ($sql !== '') {
252 $sql .= ', ';
253 }
254
255 if (isset($this->fieldMappings[$columnName])) {
256 $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]);
257 $fieldName = $this->fieldMappings[$columnName];
258 $classFieldMapping = $class->fieldMappings[$fieldName];
259 $columnSql = $tableAlias . '.' . $classFieldMapping->columnName;
260
261 $type = Type::getType($classFieldMapping->type);
262 $columnSql = $type->convertToPHPValueSQL($columnSql, $this->em->getConnection()->getDatabasePlatform());
263
264 $sql .= $columnSql;
265 } elseif (isset($this->metaMappings[$columnName])) {
266 $sql .= $tableAlias . '.' . $this->metaMappings[$columnName];
267 } elseif (isset($this->discriminatorColumns[$dqlAlias])) {
268 $sql .= $tableAlias . '.' . $this->discriminatorColumns[$dqlAlias];
269 }
270
271 $sql .= ' AS ' . $columnName;
272 }
273
274 return $sql;
275 }
276
277 public function __toString(): string
278 {
279 return $this->generateSelectClause([]);
280 }
281}
diff --git a/vendor/doctrine/orm/src/Query/SqlWalker.php b/vendor/doctrine/orm/src/Query/SqlWalker.php
new file mode 100644
index 0000000..c6f98c1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/SqlWalker.php
@@ -0,0 +1,2264 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use BadMethodCallException;
8use Doctrine\DBAL\Connection;
9use Doctrine\DBAL\LockMode;
10use Doctrine\DBAL\Platforms\AbstractPlatform;
11use Doctrine\DBAL\Types\Type;
12use Doctrine\ORM\EntityManagerInterface;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Mapping\QuoteStrategy;
15use Doctrine\ORM\OptimisticLockException;
16use Doctrine\ORM\Query;
17use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
18use Doctrine\ORM\Utility\LockSqlHelper;
19use Doctrine\ORM\Utility\PersisterHelper;
20use InvalidArgumentException;
21use LogicException;
22
23use function array_diff;
24use function array_filter;
25use function array_keys;
26use function array_map;
27use function array_merge;
28use function assert;
29use function count;
30use function implode;
31use function is_array;
32use function is_float;
33use function is_int;
34use function is_numeric;
35use function is_string;
36use function preg_match;
37use function reset;
38use function sprintf;
39use function strtolower;
40use function strtoupper;
41use function trim;
42
43/**
44 * The SqlWalker walks over a DQL AST and constructs the corresponding SQL.
45 *
46 * @psalm-import-type QueryComponent from Parser
47 * @psalm-consistent-constructor
48 */
49class SqlWalker
50{
51 use LockSqlHelper;
52
53 public const HINT_DISTINCT = 'doctrine.distinct';
54
55 private readonly ResultSetMapping $rsm;
56
57 /**
58 * Counter for generating unique column aliases.
59 */
60 private int $aliasCounter = 0;
61
62 /**
63 * Counter for generating unique table aliases.
64 */
65 private int $tableAliasCounter = 0;
66
67 /**
68 * Counter for generating unique scalar result.
69 */
70 private int $scalarResultCounter = 1;
71
72 /**
73 * Counter for generating unique parameter indexes.
74 */
75 private int $sqlParamIndex = 0;
76
77 /**
78 * Counter for generating indexes.
79 */
80 private int $newObjectCounter = 0;
81
82 private readonly EntityManagerInterface $em;
83 private readonly Connection $conn;
84
85 /** @var mixed[] */
86 private array $tableAliasMap = [];
87
88 /**
89 * Map from result variable names to their SQL column alias names.
90 *
91 * @psalm-var array<string|int, string|list<string>>
92 */
93 private array $scalarResultAliasMap = [];
94
95 /**
96 * Map from Table-Alias + Column-Name to OrderBy-Direction.
97 *
98 * @var array<string, string>
99 */
100 private array $orderedColumnsMap = [];
101
102 /**
103 * Map from DQL-Alias + Field-Name to SQL Column Alias.
104 *
105 * @var array<string, array<string, string>>
106 */
107 private array $scalarFields = [];
108
109 /**
110 * A list of classes that appear in non-scalar SelectExpressions.
111 *
112 * @psalm-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null}>
113 */
114 private array $selectedClasses = [];
115
116 /**
117 * The DQL alias of the root class of the currently traversed query.
118 *
119 * @psalm-var list<string>
120 */
121 private array $rootAliases = [];
122
123 /**
124 * Flag that indicates whether to generate SQL table aliases in the SQL.
125 * These should only be generated for SELECT queries, not for UPDATE/DELETE.
126 */
127 private bool $useSqlTableAliases = true;
128
129 /**
130 * The database platform abstraction.
131 */
132 private readonly AbstractPlatform $platform;
133
134 /**
135 * The quote strategy.
136 */
137 private readonly QuoteStrategy $quoteStrategy;
138
139 /** @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). */
140 public function __construct(
141 private readonly Query $query,
142 private readonly ParserResult $parserResult,
143 private array $queryComponents,
144 ) {
145 $this->rsm = $parserResult->getResultSetMapping();
146 $this->em = $query->getEntityManager();
147 $this->conn = $this->em->getConnection();
148 $this->platform = $this->conn->getDatabasePlatform();
149 $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy();
150 }
151
152 /**
153 * Gets the Query instance used by the walker.
154 */
155 public function getQuery(): Query
156 {
157 return $this->query;
158 }
159
160 /**
161 * Gets the Connection used by the walker.
162 */
163 public function getConnection(): Connection
164 {
165 return $this->conn;
166 }
167
168 /**
169 * Gets the EntityManager used by the walker.
170 */
171 public function getEntityManager(): EntityManagerInterface
172 {
173 return $this->em;
174 }
175
176 /**
177 * Gets the information about a single query component.
178 *
179 * @param string $dqlAlias The DQL alias.
180 *
181 * @return mixed[]
182 * @psalm-return QueryComponent
183 */
184 public function getQueryComponent(string $dqlAlias): array
185 {
186 return $this->queryComponents[$dqlAlias];
187 }
188
189 public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata
190 {
191 return $this->queryComponents[$dqlAlias]['metadata']
192 ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias));
193 }
194
195 /**
196 * Returns internal queryComponents array.
197 *
198 * @return array<string, QueryComponent>
199 */
200 public function getQueryComponents(): array
201 {
202 return $this->queryComponents;
203 }
204
205 /**
206 * Sets or overrides a query component for a given dql alias.
207 *
208 * @psalm-param QueryComponent $queryComponent
209 */
210 public function setQueryComponent(string $dqlAlias, array $queryComponent): void
211 {
212 $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token'];
213
214 if (array_diff($requiredKeys, array_keys($queryComponent))) {
215 throw QueryException::invalidQueryComponent($dqlAlias);
216 }
217
218 $this->queryComponents[$dqlAlias] = $queryComponent;
219 }
220
221 /**
222 * Gets an executor that can be used to execute the result of this walker.
223 */
224 public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor
225 {
226 return match (true) {
227 $statement instanceof AST\SelectStatement
228 => new Exec\SingleSelectExecutor($statement, $this),
229 $statement instanceof AST\UpdateStatement
230 => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined()
231 ? new Exec\MultiTableUpdateExecutor($statement, $this)
232 : new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
233 $statement instanceof AST\DeleteStatement
234 => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined()
235 ? new Exec\MultiTableDeleteExecutor($statement, $this)
236 : new Exec\SingleTableDeleteUpdateExecutor($statement, $this),
237 };
238 }
239
240 /**
241 * Generates a unique, short SQL table alias.
242 */
243 public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string
244 {
245 $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : '';
246
247 if (! isset($this->tableAliasMap[$tableName])) {
248 $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't')
249 . $this->tableAliasCounter++ . '_';
250 }
251
252 return $this->tableAliasMap[$tableName];
253 }
254
255 /**
256 * Forces the SqlWalker to use a specific alias for a table name, rather than
257 * generating an alias on its own.
258 */
259 public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string
260 {
261 $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : '';
262
263 $this->tableAliasMap[$tableName] = $alias;
264
265 return $alias;
266 }
267
268 /**
269 * Gets an SQL column alias for a column name.
270 */
271 public function getSQLColumnAlias(string $columnName): string
272 {
273 return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform);
274 }
275
276 /**
277 * Generates the SQL JOINs that are necessary for Class Table Inheritance
278 * for the given class.
279 */
280 private function generateClassTableInheritanceJoins(
281 ClassMetadata $class,
282 string $dqlAlias,
283 ): string {
284 $sql = '';
285
286 $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
287
288 // INNER JOIN parent class tables
289 foreach ($class->parentClasses as $parentClassName) {
290 $parentClass = $this->em->getClassMetadata($parentClassName);
291 $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias);
292
293 // If this is a joined association we must use left joins to preserve the correct result.
294 $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER ';
295 $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
296
297 $sqlParts = [];
298
299 foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
300 $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
301 }
302
303 // Add filters on the root class
304 $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias);
305
306 $sql .= implode(' AND ', array_filter($sqlParts));
307 }
308
309 // LEFT JOIN child class tables
310 foreach ($class->subClasses as $subClassName) {
311 $subClass = $this->em->getClassMetadata($subClassName);
312 $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
313
314 $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON ';
315
316 $sqlParts = [];
317
318 foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) {
319 $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
320 }
321
322 $sql .= implode(' AND ', $sqlParts);
323 }
324
325 return $sql;
326 }
327
328 private function generateOrderedCollectionOrderByItems(): string
329 {
330 $orderedColumns = [];
331
332 foreach ($this->selectedClasses as $selectedClass) {
333 $dqlAlias = $selectedClass['dqlAlias'];
334 $qComp = $this->queryComponents[$dqlAlias];
335
336 if (! isset($qComp['relation']->orderBy)) {
337 continue;
338 }
339
340 assert(isset($qComp['metadata']));
341 $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name);
342
343 foreach ($qComp['relation']->orderBy as $fieldName => $orientation) {
344 $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform);
345 $tableName = $qComp['metadata']->isInheritanceTypeJoined()
346 ? $persister->getOwningTable($fieldName)
347 : $qComp['metadata']->getTableName();
348
349 $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName;
350
351 // OrderByClause should replace an ordered relation. see - DDC-2475
352 if (isset($this->orderedColumnsMap[$orderedColumn])) {
353 continue;
354 }
355
356 $this->orderedColumnsMap[$orderedColumn] = $orientation;
357 $orderedColumns[] = $orderedColumn . ' ' . $orientation;
358 }
359 }
360
361 return implode(', ', $orderedColumns);
362 }
363
364 /**
365 * Generates a discriminator column SQL condition for the class with the given DQL alias.
366 *
367 * @psalm-param list<string> $dqlAliases List of root DQL aliases to inspect for discriminator restrictions.
368 */
369 private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string
370 {
371 $sqlParts = [];
372
373 foreach ($dqlAliases as $dqlAlias) {
374 $class = $this->getMetadataForDqlAlias($dqlAlias);
375
376 if (! $class->isInheritanceTypeSingleTable()) {
377 continue;
378 }
379
380 $sqlTableAlias = $this->useSqlTableAliases
381 ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'
382 : '';
383
384 $conn = $this->em->getConnection();
385 $values = [];
386
387 if ($class->discriminatorValue !== null) { // discriminators can be 0
388 $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue)
389 ? $class->discriminatorValue
390 : $conn->quote((string) $class->discriminatorValue);
391 }
392
393 foreach ($class->subClasses as $subclassName) {
394 $subclassMetadata = $this->em->getClassMetadata($subclassName);
395
396 // Abstract entity classes show up in the list of subClasses, but may be omitted
397 // from the discriminator map. In that case, they have a null discriminator value.
398 if ($subclassMetadata->discriminatorValue === null) {
399 continue;
400 }
401
402 $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue)
403 ? $subclassMetadata->discriminatorValue
404 : $conn->quote((string) $subclassMetadata->discriminatorValue);
405 }
406
407 if ($values !== []) {
408 $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')';
409 } else {
410 $sqlParts[] = '1=0'; // impossible condition
411 }
412 }
413
414 $sql = implode(' AND ', $sqlParts);
415
416 return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql;
417 }
418
419 /**
420 * Generates the filter SQL for a given entity and table alias.
421 */
422 private function generateFilterConditionSQL(
423 ClassMetadata $targetEntity,
424 string $targetTableAlias,
425 ): string {
426 if (! $this->em->hasFilters()) {
427 return '';
428 }
429
430 switch ($targetEntity->inheritanceType) {
431 case ClassMetadata::INHERITANCE_TYPE_NONE:
432 break;
433 case ClassMetadata::INHERITANCE_TYPE_JOINED:
434 // The classes in the inheritance will be added to the query one by one,
435 // but only the root node is getting filtered
436 if ($targetEntity->name !== $targetEntity->rootEntityName) {
437 return '';
438 }
439
440 break;
441 case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE:
442 // With STI the table will only be queried once, make sure that the filters
443 // are added to the root entity
444 $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName);
445 break;
446 default:
447 //@todo: throw exception?
448 return '';
449 }
450
451 $filterClauses = [];
452 foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
453 $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
454 if ($filterExpr !== '') {
455 $filterClauses[] = '(' . $filterExpr . ')';
456 }
457 }
458
459 return implode(' AND ', $filterClauses);
460 }
461
462 /**
463 * Walks down a SelectStatement AST node, thereby generating the appropriate SQL.
464 */
465 public function walkSelectStatement(AST\SelectStatement $selectStatement): string
466 {
467 $limit = $this->query->getMaxResults();
468 $offset = $this->query->getFirstResult();
469 $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;
470 $sql = $this->walkSelectClause($selectStatement->selectClause)
471 . $this->walkFromClause($selectStatement->fromClause)
472 . $this->walkWhereClause($selectStatement->whereClause);
473
474 if ($selectStatement->groupByClause) {
475 $sql .= $this->walkGroupByClause($selectStatement->groupByClause);
476 }
477
478 if ($selectStatement->havingClause) {
479 $sql .= $this->walkHavingClause($selectStatement->havingClause);
480 }
481
482 if ($selectStatement->orderByClause) {
483 $sql .= $this->walkOrderByClause($selectStatement->orderByClause);
484 }
485
486 $orderBySql = $this->generateOrderedCollectionOrderByItems();
487 if (! $selectStatement->orderByClause && $orderBySql) {
488 $sql .= ' ORDER BY ' . $orderBySql;
489 }
490
491 $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset);
492
493 if ($lockMode === LockMode::NONE) {
494 return $sql;
495 }
496
497 if ($lockMode === LockMode::PESSIMISTIC_READ) {
498 return $sql . ' ' . $this->getReadLockSQL($this->platform);
499 }
500
501 if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
502 return $sql . ' ' . $this->getWriteLockSQL($this->platform);
503 }
504
505 if ($lockMode !== LockMode::OPTIMISTIC) {
506 throw QueryException::invalidLockMode();
507 }
508
509 foreach ($this->selectedClasses as $selectedClass) {
510 if (! $selectedClass['class']->isVersioned) {
511 throw OptimisticLockException::lockFailed($selectedClass['class']->name);
512 }
513 }
514
515 return $sql;
516 }
517
518 /**
519 * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL.
520 */
521 public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string
522 {
523 $this->useSqlTableAliases = false;
524 $this->rsm->isSelect = false;
525
526 return $this->walkUpdateClause($updateStatement->updateClause)
527 . $this->walkWhereClause($updateStatement->whereClause);
528 }
529
530 /**
531 * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL.
532 */
533 public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string
534 {
535 $this->useSqlTableAliases = false;
536 $this->rsm->isSelect = false;
537
538 return $this->walkDeleteClause($deleteStatement->deleteClause)
539 . $this->walkWhereClause($deleteStatement->whereClause);
540 }
541
542 /**
543 * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL.
544 * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers.
545 */
546 public function walkEntityIdentificationVariable(string $identVariable): string
547 {
548 $class = $this->getMetadataForDqlAlias($identVariable);
549 $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable);
550 $sqlParts = [];
551
552 foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
553 $sqlParts[] = $tableAlias . '.' . $columnName;
554 }
555
556 return implode(', ', $sqlParts);
557 }
558
559 /**
560 * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
561 */
562 public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string
563 {
564 $class = $this->getMetadataForDqlAlias($identificationVariable);
565
566 if (
567 $fieldName !== null && $class->isInheritanceTypeJoined() &&
568 isset($class->fieldMappings[$fieldName]->inherited)
569 ) {
570 $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited);
571 }
572
573 return $this->getSQLTableAlias($class->getTableName(), $identificationVariable);
574 }
575
576 /**
577 * Walks down a PathExpression AST node, thereby generating the appropriate SQL.
578 */
579 public function walkPathExpression(AST\PathExpression $pathExpr): string
580 {
581 $sql = '';
582 assert($pathExpr->field !== null);
583
584 switch ($pathExpr->type) {
585 case AST\PathExpression::TYPE_STATE_FIELD:
586 $fieldName = $pathExpr->field;
587 $dqlAlias = $pathExpr->identificationVariable;
588 $class = $this->getMetadataForDqlAlias($dqlAlias);
589
590 if ($this->useSqlTableAliases) {
591 $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.';
592 }
593
594 $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
595 break;
596
597 case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION:
598 // 1- the owning side:
599 // Just use the foreign key, i.e. u.group_id
600 $fieldName = $pathExpr->field;
601 $dqlAlias = $pathExpr->identificationVariable;
602 $class = $this->getMetadataForDqlAlias($dqlAlias);
603
604 if (isset($class->associationMappings[$fieldName]->inherited)) {
605 $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited);
606 }
607
608 $assoc = $class->associationMappings[$fieldName];
609
610 if (! $assoc->isOwningSide()) {
611 throw QueryException::associationPathInverseSideNotSupported($pathExpr);
612 }
613
614 assert($assoc->isToOneOwningSide());
615
616 // COMPOSITE KEYS NOT (YET?) SUPPORTED
617 if (count($assoc->sourceToTargetKeyColumns) > 1) {
618 throw QueryException::associationPathCompositeKeyNotSupported();
619 }
620
621 if ($this->useSqlTableAliases) {
622 $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.';
623 }
624
625 $sql .= reset($assoc->targetToSourceKeyColumns);
626 break;
627
628 default:
629 throw QueryException::invalidPathExpression($pathExpr);
630 }
631
632 return $sql;
633 }
634
635 /**
636 * Walks down a SelectClause AST node, thereby generating the appropriate SQL.
637 */
638 public function walkSelectClause(AST\SelectClause $selectClause): string
639 {
640 $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : '');
641 $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions));
642
643 if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) {
644 $this->query->setHint(self::HINT_DISTINCT, true);
645 }
646
647 $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT
648 || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
649
650 foreach ($this->selectedClasses as $selectedClass) {
651 $class = $selectedClass['class'];
652 $dqlAlias = $selectedClass['dqlAlias'];
653 $resultAlias = $selectedClass['resultAlias'];
654
655 // Register as entity or joined entity result
656 if (! isset($this->queryComponents[$dqlAlias]['relation'])) {
657 $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias);
658 } else {
659 assert(isset($this->queryComponents[$dqlAlias]['parent']));
660
661 $this->rsm->addJoinedEntityResult(
662 $class->name,
663 $dqlAlias,
664 $this->queryComponents[$dqlAlias]['parent'],
665 $this->queryComponents[$dqlAlias]['relation']->fieldName,
666 );
667 }
668
669 if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) {
670 // Add discriminator columns to SQL
671 $rootClass = $this->em->getClassMetadata($class->rootEntityName);
672 $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias);
673 $discrColumn = $rootClass->getDiscriminatorColumn();
674 $columnAlias = $this->getSQLColumnAlias($discrColumn->name);
675
676 $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias;
677
678 $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias);
679 $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type);
680 if (! empty($discrColumn->enumType)) {
681 $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType);
682 }
683 }
684
685 // Add foreign key columns to SQL, if necessary
686 if (! $addMetaColumns && ! $class->containsForeignIdentifier) {
687 continue;
688 }
689
690 // Add foreign key columns of class and also parent classes
691 foreach ($class->associationMappings as $assoc) {
692 if (
693 ! $assoc->isToOneOwningSide()
694 || ( ! $addMetaColumns && ! isset($assoc->id))
695 ) {
696 continue;
697 }
698
699 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
700 $isIdentifier = (isset($assoc->id) && $assoc->id === true);
701 $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class;
702 $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias);
703
704 foreach ($assoc->joinColumns as $joinColumn) {
705 $columnName = $joinColumn->name;
706 $columnAlias = $this->getSQLColumnAlias($columnName);
707 $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
708
709 $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
710 $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
711
712 $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType);
713 }
714 }
715
716 // Add foreign key columns to SQL, if necessary
717 if (! $addMetaColumns) {
718 continue;
719 }
720
721 // Add foreign key columns of subclasses
722 foreach ($class->subClasses as $subClassName) {
723 $subClass = $this->em->getClassMetadata($subClassName);
724 $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
725
726 foreach ($subClass->associationMappings as $assoc) {
727 // Skip if association is inherited
728 if (isset($assoc->inherited)) {
729 continue;
730 }
731
732 if ($assoc->isToOneOwningSide()) {
733 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
734
735 foreach ($assoc->joinColumns as $joinColumn) {
736 $columnName = $joinColumn->name;
737 $columnAlias = $this->getSQLColumnAlias($columnName);
738 $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
739
740 $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform);
741 $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
742
743 $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType);
744 }
745 }
746 }
747 }
748 }
749
750 return $sql . implode(', ', $sqlSelectExpressions);
751 }
752
753 /**
754 * Walks down a FromClause AST node, thereby generating the appropriate SQL.
755 */
756 public function walkFromClause(AST\FromClause $fromClause): string
757 {
758 $identificationVarDecls = $fromClause->identificationVariableDeclarations;
759 $sqlParts = [];
760
761 foreach ($identificationVarDecls as $identificationVariableDecl) {
762 $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl);
763 }
764
765 return ' FROM ' . implode(', ', $sqlParts);
766 }
767
768 /**
769 * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL.
770 */
771 public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string
772 {
773 $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration);
774
775 if ($identificationVariableDecl->indexBy) {
776 $this->walkIndexBy($identificationVariableDecl->indexBy);
777 }
778
779 foreach ($identificationVariableDecl->joins as $join) {
780 $sql .= $this->walkJoin($join);
781 }
782
783 return $sql;
784 }
785
786 /**
787 * Walks down a IndexBy AST node.
788 */
789 public function walkIndexBy(AST\IndexBy $indexBy): void
790 {
791 $pathExpression = $indexBy->singleValuedPathExpression;
792 $alias = $pathExpression->identificationVariable;
793 assert($pathExpression->field !== null);
794
795 switch ($pathExpression->type) {
796 case AST\PathExpression::TYPE_STATE_FIELD:
797 $field = $pathExpression->field;
798 break;
799
800 case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION:
801 // Just use the foreign key, i.e. u.group_id
802 $fieldName = $pathExpression->field;
803 $class = $this->getMetadataForDqlAlias($alias);
804
805 if (isset($class->associationMappings[$fieldName]->inherited)) {
806 $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited);
807 }
808
809 $association = $class->associationMappings[$fieldName];
810
811 if (! $association->isOwningSide()) {
812 throw QueryException::associationPathInverseSideNotSupported($pathExpression);
813 }
814
815 assert($association->isToOneOwningSide());
816
817 if (count($association->sourceToTargetKeyColumns) > 1) {
818 throw QueryException::associationPathCompositeKeyNotSupported();
819 }
820
821 $field = reset($association->targetToSourceKeyColumns);
822 break;
823
824 default:
825 throw QueryException::invalidPathExpression($pathExpression);
826 }
827
828 if (isset($this->scalarFields[$alias][$field])) {
829 $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]);
830
831 return;
832 }
833
834 $this->rsm->addIndexBy($alias, $field);
835 }
836
837 /**
838 * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL.
839 */
840 public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string
841 {
842 return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false);
843 }
844
845 /**
846 * Generate appropriate SQL for RangeVariableDeclaration AST node
847 */
848 private function generateRangeVariableDeclarationSQL(
849 AST\RangeVariableDeclaration $rangeVariableDeclaration,
850 bool $buildNestedJoins,
851 ): string {
852 $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName);
853 $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable;
854
855 if ($rangeVariableDeclaration->isRoot) {
856 $this->rootAliases[] = $dqlAlias;
857 }
858
859 $sql = $this->platform->appendLockHint(
860 $this->quoteStrategy->getTableName($class, $this->platform) . ' ' .
861 $this->getSQLTableAlias($class->getTableName(), $dqlAlias),
862 $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE,
863 );
864
865 if (! $class->isInheritanceTypeJoined()) {
866 return $sql;
867 }
868
869 $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias);
870
871 if (! $buildNestedJoins) {
872 return $sql . $classTableInheritanceJoins;
873 }
874
875 return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')';
876 }
877
878 /**
879 * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
880 *
881 * @psalm-param AST\Join::JOIN_TYPE_* $joinType
882 *
883 * @throws QueryException
884 */
885 public function walkJoinAssociationDeclaration(
886 AST\JoinAssociationDeclaration $joinAssociationDeclaration,
887 int $joinType = AST\Join::JOIN_TYPE_INNER,
888 AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null,
889 ): string {
890 $sql = '';
891
892 $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression;
893 $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable;
894 $indexBy = $joinAssociationDeclaration->indexBy;
895
896 $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null;
897 assert($relation !== null);
898 $targetClass = $this->em->getClassMetadata($relation->targetEntity);
899 $sourceClass = $this->em->getClassMetadata($relation->sourceEntity);
900 $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
901
902 $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
903 $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable);
904
905 // Ensure we got the owning side, since it has all mapping info
906 $assoc = $this->em->getMetadataFactory()->getOwningSide($relation);
907
908 if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) {
909 if ($relation->isToMany()) {
910 throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
911 }
912 }
913
914 $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch;
915
916 if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
917 throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName);
918 }
919
920 // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
921 // be the owning side and previously we ensured that $assoc is always the owning side of the associations.
922 // The owning side is necessary at this point because only it contains the JoinColumn information.
923 switch (true) {
924 case $assoc->isToOne():
925 assert($assoc->isToOneOwningSide());
926 $conditions = [];
927
928 foreach ($assoc->joinColumns as $joinColumn) {
929 $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
930 $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
931
932 if ($relation->isOwningSide()) {
933 $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn;
934
935 continue;
936 }
937
938 $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn;
939 }
940
941 // Apply remaining inheritance restrictions
942 $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
943
944 if ($discrSql) {
945 $conditions[] = $discrSql;
946 }
947
948 // Apply the filters
949 $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
950
951 if ($filterExpr) {
952 $conditions[] = $filterExpr;
953 }
954
955 $targetTableJoin = [
956 'table' => $targetTableName . ' ' . $targetTableAlias,
957 'condition' => implode(' AND ', $conditions),
958 ];
959 break;
960
961 case $assoc->isManyToMany():
962 // Join relation table
963 $joinTable = $assoc->joinTable;
964 $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias);
965 $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform);
966
967 $conditions = [];
968 $relationColumns = $relation->isOwningSide()
969 ? $assoc->joinTable->joinColumns
970 : $assoc->joinTable->inverseJoinColumns;
971
972 foreach ($relationColumns as $joinColumn) {
973 $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
974 $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
975
976 $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
977 }
978
979 $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions);
980
981 // Join target table
982 $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN ';
983
984 $conditions = [];
985 $relationColumns = $relation->isOwningSide()
986 ? $assoc->joinTable->inverseJoinColumns
987 : $assoc->joinTable->joinColumns;
988
989 foreach ($relationColumns as $joinColumn) {
990 $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
991 $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
992
993 $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
994 }
995
996 // Apply remaining inheritance restrictions
997 $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
998
999 if ($discrSql) {
1000 $conditions[] = $discrSql;
1001 }
1002
1003 // Apply the filters
1004 $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
1005
1006 if ($filterExpr) {
1007 $conditions[] = $filterExpr;
1008 }
1009
1010 $targetTableJoin = [
1011 'table' => $targetTableName . ' ' . $targetTableAlias,
1012 'condition' => implode(' AND ', $conditions),
1013 ];
1014 break;
1015
1016 default:
1017 throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY');
1018 }
1019
1020 // Handle WITH clause
1021 $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')');
1022
1023 if ($targetClass->isInheritanceTypeJoined()) {
1024 $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias);
1025 // If we have WITH condition, we need to build nested joins for target class table and cti joins
1026 if ($withCondition && $ctiJoins) {
1027 $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition'];
1028 } else {
1029 $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins;
1030 }
1031 } else {
1032 $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'];
1033 }
1034
1035 if ($withCondition) {
1036 $sql .= ' AND ' . $withCondition;
1037 }
1038
1039 // Apply the indexes
1040 if ($indexBy) {
1041 // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently.
1042 $this->walkIndexBy($indexBy);
1043 } elseif ($relation->isIndexed()) {
1044 $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy());
1045 }
1046
1047 return $sql;
1048 }
1049
1050 /**
1051 * Walks down a FunctionNode AST node, thereby generating the appropriate SQL.
1052 */
1053 public function walkFunction(AST\Functions\FunctionNode $function): string
1054 {
1055 return $function->getSql($this);
1056 }
1057
1058 /**
1059 * Walks down an OrderByClause AST node, thereby generating the appropriate SQL.
1060 */
1061 public function walkOrderByClause(AST\OrderByClause $orderByClause): string
1062 {
1063 $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems);
1064
1065 $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems();
1066 if ($collectionOrderByItems !== '') {
1067 $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems);
1068 }
1069
1070 return ' ORDER BY ' . implode(', ', $orderByItems);
1071 }
1072
1073 /**
1074 * Walks down an OrderByItem AST node, thereby generating the appropriate SQL.
1075 */
1076 public function walkOrderByItem(AST\OrderByItem $orderByItem): string
1077 {
1078 $type = strtoupper($orderByItem->type);
1079 $expr = $orderByItem->expression;
1080 $sql = $expr instanceof AST\Node
1081 ? $expr->dispatch($this)
1082 : $this->walkResultVariable($this->queryComponents[$expr]['token']->value);
1083
1084 $this->orderedColumnsMap[$sql] = $type;
1085
1086 if ($expr instanceof AST\Subselect) {
1087 return '(' . $sql . ') ' . $type;
1088 }
1089
1090 return $sql . ' ' . $type;
1091 }
1092
1093 /**
1094 * Walks down a HavingClause AST node, thereby generating the appropriate SQL.
1095 */
1096 public function walkHavingClause(AST\HavingClause $havingClause): string
1097 {
1098 return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression);
1099 }
1100
1101 /**
1102 * Walks down a Join AST node and creates the corresponding SQL.
1103 */
1104 public function walkJoin(AST\Join $join): string
1105 {
1106 $joinType = $join->joinType;
1107 $joinDeclaration = $join->joinAssociationDeclaration;
1108
1109 $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER
1110 ? ' LEFT JOIN '
1111 : ' INNER JOIN ';
1112
1113 switch (true) {
1114 case $joinDeclaration instanceof AST\RangeVariableDeclaration:
1115 $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName);
1116 $dqlAlias = $joinDeclaration->aliasIdentificationVariable;
1117 $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1118 $conditions = [];
1119
1120 if ($join->conditionalExpression) {
1121 $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
1122 }
1123
1124 $isUnconditionalJoin = $conditions === [];
1125 $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin
1126 ? ' AND '
1127 : ' ON ';
1128
1129 $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin);
1130
1131 // Apply remaining inheritance restrictions
1132 $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]);
1133
1134 if ($discrSql) {
1135 $conditions[] = $discrSql;
1136 }
1137
1138 // Apply the filters
1139 $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias);
1140
1141 if ($filterExpr) {
1142 $conditions[] = $filterExpr;
1143 }
1144
1145 if ($conditions) {
1146 $sql .= $condExprConjunction . implode(' AND ', $conditions);
1147 }
1148
1149 break;
1150
1151 case $joinDeclaration instanceof AST\JoinAssociationDeclaration:
1152 $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression);
1153 break;
1154 }
1155
1156 return $sql;
1157 }
1158
1159 /**
1160 * Walks down a CoalesceExpression AST node and generates the corresponding SQL.
1161 */
1162 public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string
1163 {
1164 $sql = 'COALESCE(';
1165
1166 $scalarExpressions = [];
1167
1168 foreach ($coalesceExpression->scalarExpressions as $scalarExpression) {
1169 $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression);
1170 }
1171
1172 return $sql . implode(', ', $scalarExpressions) . ')';
1173 }
1174
1175 /**
1176 * Walks down a NullIfExpression AST node and generates the corresponding SQL.
1177 */
1178 public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string
1179 {
1180 $firstExpression = is_string($nullIfExpression->firstExpression)
1181 ? $this->conn->quote($nullIfExpression->firstExpression)
1182 : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression);
1183
1184 $secondExpression = is_string($nullIfExpression->secondExpression)
1185 ? $this->conn->quote($nullIfExpression->secondExpression)
1186 : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression);
1187
1188 return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')';
1189 }
1190
1191 /**
1192 * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL.
1193 */
1194 public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string
1195 {
1196 $sql = 'CASE';
1197
1198 foreach ($generalCaseExpression->whenClauses as $whenClause) {
1199 $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression);
1200 $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression);
1201 }
1202
1203 $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END';
1204
1205 return $sql;
1206 }
1207
1208 /**
1209 * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL.
1210 */
1211 public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string
1212 {
1213 $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand);
1214
1215 foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) {
1216 $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression);
1217 $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression);
1218 }
1219
1220 $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END';
1221
1222 return $sql;
1223 }
1224
1225 /**
1226 * Walks down a SelectExpression AST node and generates the corresponding SQL.
1227 */
1228 public function walkSelectExpression(AST\SelectExpression $selectExpression): string
1229 {
1230 $sql = '';
1231 $expr = $selectExpression->expression;
1232 $hidden = $selectExpression->hiddenAliasResultVariable;
1233
1234 switch (true) {
1235 case $expr instanceof AST\PathExpression:
1236 if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) {
1237 throw QueryException::invalidPathExpression($expr);
1238 }
1239
1240 assert($expr->field !== null);
1241 $fieldName = $expr->field;
1242 $dqlAlias = $expr->identificationVariable;
1243 $class = $this->getMetadataForDqlAlias($dqlAlias);
1244
1245 $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName;
1246 $tableName = $class->isInheritanceTypeJoined()
1247 ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName)
1248 : $class->getTableName();
1249
1250 $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
1251 $fieldMapping = $class->fieldMappings[$fieldName];
1252 $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1253 $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
1254 $col = $sqlTableAlias . '.' . $columnName;
1255
1256 $type = Type::getType($fieldMapping->type);
1257 $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform());
1258
1259 $sql .= $col . ' AS ' . $columnAlias;
1260
1261 $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1262
1263 if (! $hidden) {
1264 $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type);
1265 $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias;
1266
1267 if (! empty($fieldMapping->enumType)) {
1268 $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType);
1269 }
1270 }
1271
1272 break;
1273
1274 case $expr instanceof AST\AggregateExpression:
1275 case $expr instanceof AST\Functions\FunctionNode:
1276 case $expr instanceof AST\SimpleArithmeticExpression:
1277 case $expr instanceof AST\ArithmeticTerm:
1278 case $expr instanceof AST\ArithmeticFactor:
1279 case $expr instanceof AST\ParenthesisExpression:
1280 case $expr instanceof AST\Literal:
1281 case $expr instanceof AST\NullIfExpression:
1282 case $expr instanceof AST\CoalesceExpression:
1283 case $expr instanceof AST\GeneralCaseExpression:
1284 case $expr instanceof AST\SimpleCaseExpression:
1285 $columnAlias = $this->getSQLColumnAlias('sclr');
1286 $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1287
1288 $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1289
1290 $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1291
1292 if ($hidden) {
1293 break;
1294 }
1295
1296 if (! $expr instanceof Query\AST\TypedExpression) {
1297 // Conceptually we could resolve field type here by traverse through AST to retrieve field type,
1298 // but this is not a feasible solution; assume 'string'.
1299 $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1300
1301 break;
1302 }
1303
1304 $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType()));
1305
1306 break;
1307
1308 case $expr instanceof AST\Subselect:
1309 $columnAlias = $this->getSQLColumnAlias('sclr');
1310 $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1311
1312 $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1313
1314 $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1315
1316 if (! $hidden) {
1317 // We cannot resolve field type here; assume 'string'.
1318 $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1319 }
1320
1321 break;
1322
1323 case $expr instanceof AST\NewObjectExpression:
1324 $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable);
1325 break;
1326
1327 default:
1328 $dqlAlias = $expr;
1329 $class = $this->getMetadataForDqlAlias($dqlAlias);
1330 $resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
1331
1332 if (! isset($this->selectedClasses[$dqlAlias])) {
1333 $this->selectedClasses[$dqlAlias] = [
1334 'class' => $class,
1335 'dqlAlias' => $dqlAlias,
1336 'resultAlias' => $resultAlias,
1337 ];
1338 }
1339
1340 $sqlParts = [];
1341
1342 // Select all fields from the queried class
1343 foreach ($class->fieldMappings as $fieldName => $mapping) {
1344 $tableName = isset($mapping->inherited)
1345 ? $this->em->getClassMetadata($mapping->inherited)->getTableName()
1346 : $class->getTableName();
1347
1348 $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
1349 $columnAlias = $this->getSQLColumnAlias($mapping->columnName);
1350 $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1351
1352 $col = $sqlTableAlias . '.' . $quotedColumnName;
1353
1354 $type = Type::getType($mapping->type);
1355 $col = $type->convertToPHPValueSQL($col, $this->platform);
1356
1357 $sqlParts[] = $col . ' AS ' . $columnAlias;
1358
1359 $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1360
1361 $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
1362
1363 if (! empty($mapping->enumType)) {
1364 $this->rsm->addEnumResult($columnAlias, $mapping->enumType);
1365 }
1366 }
1367
1368 // Add any additional fields of subclasses (excluding inherited fields)
1369 // 1) on Single Table Inheritance: always, since its marginal overhead
1370 // 2) on Class Table Inheritance
1371 foreach ($class->subClasses as $subClassName) {
1372 $subClass = $this->em->getClassMetadata($subClassName);
1373 $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
1374
1375 foreach ($subClass->fieldMappings as $fieldName => $mapping) {
1376 if (isset($mapping->inherited)) {
1377 continue;
1378 }
1379
1380 $columnAlias = $this->getSQLColumnAlias($mapping->columnName);
1381 $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
1382
1383 $col = $sqlTableAlias . '.' . $quotedColumnName;
1384
1385 $type = Type::getType($mapping->type);
1386 $col = $type->convertToPHPValueSQL($col, $this->platform);
1387
1388 $sqlParts[] = $col . ' AS ' . $columnAlias;
1389
1390 $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1391
1392 $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
1393 }
1394 }
1395
1396 $sql .= implode(', ', $sqlParts);
1397 }
1398
1399 return $sql;
1400 }
1401
1402 public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string
1403 {
1404 return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')';
1405 }
1406
1407 /**
1408 * Walks down a Subselect AST node, thereby generating the appropriate SQL.
1409 */
1410 public function walkSubselect(AST\Subselect $subselect): string
1411 {
1412 $useAliasesBefore = $this->useSqlTableAliases;
1413 $rootAliasesBefore = $this->rootAliases;
1414
1415 $this->rootAliases = []; // reset the rootAliases for the subselect
1416 $this->useSqlTableAliases = true;
1417
1418 $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause);
1419 $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause);
1420 $sql .= $this->walkWhereClause($subselect->whereClause);
1421
1422 $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : '';
1423 $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : '';
1424 $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : '';
1425
1426 $this->rootAliases = $rootAliasesBefore; // put the main aliases back
1427 $this->useSqlTableAliases = $useAliasesBefore;
1428
1429 return $sql;
1430 }
1431
1432 /**
1433 * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL.
1434 */
1435 public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string
1436 {
1437 $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations;
1438 $sqlParts = [];
1439
1440 foreach ($identificationVarDecls as $subselectIdVarDecl) {
1441 $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl);
1442 }
1443
1444 return ' FROM ' . implode(', ', $sqlParts);
1445 }
1446
1447 /**
1448 * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL.
1449 */
1450 public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string
1451 {
1452 return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '')
1453 . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression);
1454 }
1455
1456 public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string
1457 {
1458 return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this));
1459 }
1460
1461 public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string
1462 {
1463 $sqlSelectExpressions = [];
1464 $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
1465
1466 foreach ($newObjectExpression->args as $argIndex => $e) {
1467 $resultAlias = $this->scalarResultCounter++;
1468 $columnAlias = $this->getSQLColumnAlias('sclr');
1469 $fieldType = 'string';
1470
1471 switch (true) {
1472 case $e instanceof AST\NewObjectExpression:
1473 $sqlSelectExpressions[] = $e->dispatch($this);
1474 break;
1475
1476 case $e instanceof AST\Subselect:
1477 $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias;
1478 break;
1479
1480 case $e instanceof AST\PathExpression:
1481 assert($e->field !== null);
1482 $dqlAlias = $e->identificationVariable;
1483 $class = $this->getMetadataForDqlAlias($dqlAlias);
1484 $fieldName = $e->field;
1485 $fieldMapping = $class->fieldMappings[$fieldName];
1486 $fieldType = $fieldMapping->type;
1487 $col = trim($e->dispatch($this));
1488
1489 $type = Type::getType($fieldType);
1490 $col = $type->convertToPHPValueSQL($col, $this->platform);
1491
1492 $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias;
1493
1494 if (! empty($fieldMapping->enumType)) {
1495 $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType);
1496 }
1497
1498 break;
1499
1500 case $e instanceof AST\Literal:
1501 switch ($e->type) {
1502 case AST\Literal::BOOLEAN:
1503 $fieldType = 'boolean';
1504 break;
1505
1506 case AST\Literal::NUMERIC:
1507 $fieldType = is_float($e->value) ? 'float' : 'integer';
1508 break;
1509 }
1510
1511 $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1512 break;
1513
1514 default:
1515 $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1516 break;
1517 }
1518
1519 $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1520 $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
1521
1522 $this->rsm->newObjectMappings[$columnAlias] = [
1523 'className' => $newObjectExpression->className,
1524 'objIndex' => $objIndex,
1525 'argIndex' => $argIndex,
1526 ];
1527 }
1528
1529 return implode(', ', $sqlSelectExpressions);
1530 }
1531
1532 /**
1533 * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL.
1534 */
1535 public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string
1536 {
1537 $expr = $simpleSelectExpression->expression;
1538 $sql = ' ';
1539
1540 switch (true) {
1541 case $expr instanceof AST\PathExpression:
1542 $sql .= $this->walkPathExpression($expr);
1543 break;
1544
1545 case $expr instanceof AST\Subselect:
1546 $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1547
1548 $columnAlias = 'sclr' . $this->aliasCounter++;
1549 $this->scalarResultAliasMap[$alias] = $columnAlias;
1550
1551 $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1552 break;
1553
1554 case $expr instanceof AST\Functions\FunctionNode:
1555 case $expr instanceof AST\SimpleArithmeticExpression:
1556 case $expr instanceof AST\ArithmeticTerm:
1557 case $expr instanceof AST\ArithmeticFactor:
1558 case $expr instanceof AST\Literal:
1559 case $expr instanceof AST\NullIfExpression:
1560 case $expr instanceof AST\CoalesceExpression:
1561 case $expr instanceof AST\GeneralCaseExpression:
1562 case $expr instanceof AST\SimpleCaseExpression:
1563 $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1564
1565 $columnAlias = $this->getSQLColumnAlias('sclr');
1566 $this->scalarResultAliasMap[$alias] = $columnAlias;
1567
1568 $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1569 break;
1570
1571 case $expr instanceof AST\ParenthesisExpression:
1572 $sql .= $this->walkParenthesisExpression($expr);
1573 break;
1574
1575 default: // IdentificationVariable
1576 $sql .= $this->walkEntityIdentificationVariable($expr);
1577 break;
1578 }
1579
1580 return $sql;
1581 }
1582
1583 /**
1584 * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL.
1585 */
1586 public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string
1587 {
1588 return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '')
1589 . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')';
1590 }
1591
1592 /**
1593 * Walks down a GroupByClause AST node, thereby generating the appropriate SQL.
1594 */
1595 public function walkGroupByClause(AST\GroupByClause $groupByClause): string
1596 {
1597 $sqlParts = [];
1598
1599 foreach ($groupByClause->groupByItems as $groupByItem) {
1600 $sqlParts[] = $this->walkGroupByItem($groupByItem);
1601 }
1602
1603 return ' GROUP BY ' . implode(', ', $sqlParts);
1604 }
1605
1606 /**
1607 * Walks down a GroupByItem AST node, thereby generating the appropriate SQL.
1608 */
1609 public function walkGroupByItem(AST\PathExpression|string $groupByItem): string
1610 {
1611 // StateFieldPathExpression
1612 if (! is_string($groupByItem)) {
1613 return $this->walkPathExpression($groupByItem);
1614 }
1615
1616 // ResultVariable
1617 if (isset($this->queryComponents[$groupByItem]['resultVariable'])) {
1618 $resultVariable = $this->queryComponents[$groupByItem]['resultVariable'];
1619
1620 if ($resultVariable instanceof AST\PathExpression) {
1621 return $this->walkPathExpression($resultVariable);
1622 }
1623
1624 if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) {
1625 return $this->walkPathExpression($resultVariable->pathExpression);
1626 }
1627
1628 return $this->walkResultVariable($groupByItem);
1629 }
1630
1631 // IdentificationVariable
1632 $sqlParts = [];
1633
1634 foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) {
1635 $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field);
1636 $item->type = AST\PathExpression::TYPE_STATE_FIELD;
1637
1638 $sqlParts[] = $this->walkPathExpression($item);
1639 }
1640
1641 foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) {
1642 if ($mapping->isToOneOwningSide()) {
1643 $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName);
1644 $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
1645
1646 $sqlParts[] = $this->walkPathExpression($item);
1647 }
1648 }
1649
1650 return implode(', ', $sqlParts);
1651 }
1652
1653 /**
1654 * Walks down a DeleteClause AST node, thereby generating the appropriate SQL.
1655 */
1656 public function walkDeleteClause(AST\DeleteClause $deleteClause): string
1657 {
1658 $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1659 $tableName = $class->getTableName();
1660 $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform);
1661
1662 $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable);
1663 $this->rootAliases[] = $deleteClause->aliasIdentificationVariable;
1664
1665 return $sql;
1666 }
1667
1668 /**
1669 * Walks down an UpdateClause AST node, thereby generating the appropriate SQL.
1670 */
1671 public function walkUpdateClause(AST\UpdateClause $updateClause): string
1672 {
1673 $class = $this->em->getClassMetadata($updateClause->abstractSchemaName);
1674 $tableName = $class->getTableName();
1675 $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform);
1676
1677 $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable);
1678 $this->rootAliases[] = $updateClause->aliasIdentificationVariable;
1679
1680 return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems));
1681 }
1682
1683 /**
1684 * Walks down an UpdateItem AST node, thereby generating the appropriate SQL.
1685 */
1686 public function walkUpdateItem(AST\UpdateItem $updateItem): string
1687 {
1688 $useTableAliasesBefore = $this->useSqlTableAliases;
1689 $this->useSqlTableAliases = false;
1690
1691 $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = ';
1692 $newValue = $updateItem->newValue;
1693
1694 $sql .= match (true) {
1695 $newValue instanceof AST\Node => $newValue->dispatch($this),
1696 $newValue === null => 'NULL',
1697 };
1698
1699 $this->useSqlTableAliases = $useTableAliasesBefore;
1700
1701 return $sql;
1702 }
1703
1704 /**
1705 * Walks down a WhereClause AST node, thereby generating the appropriate SQL.
1706 *
1707 * WhereClause or not, the appropriate discriminator sql is added.
1708 */
1709 public function walkWhereClause(AST\WhereClause|null $whereClause): string
1710 {
1711 $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : '';
1712 $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases);
1713
1714 if ($this->em->hasFilters()) {
1715 $filterClauses = [];
1716 foreach ($this->rootAliases as $dqlAlias) {
1717 $class = $this->getMetadataForDqlAlias($dqlAlias);
1718 $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1719
1720 $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias);
1721 if ($filterExpr) {
1722 $filterClauses[] = $filterExpr;
1723 }
1724 }
1725
1726 if (count($filterClauses)) {
1727 if ($condSql) {
1728 $condSql = '(' . $condSql . ') AND ';
1729 }
1730
1731 $condSql .= implode(' AND ', $filterClauses);
1732 }
1733 }
1734
1735 if ($condSql) {
1736 return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql);
1737 }
1738
1739 if ($discrSql) {
1740 return ' WHERE ' . $discrSql;
1741 }
1742
1743 return '';
1744 }
1745
1746 /**
1747 * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL.
1748 */
1749 public function walkConditionalExpression(
1750 AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr,
1751 ): string {
1752 // Phase 2 AST optimization: Skip processing of ConditionalExpression
1753 // if only one ConditionalTerm is defined
1754 if (! ($condExpr instanceof AST\ConditionalExpression)) {
1755 return $this->walkConditionalTerm($condExpr);
1756 }
1757
1758 return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms));
1759 }
1760
1761 /**
1762 * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL.
1763 */
1764 public function walkConditionalTerm(
1765 AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm,
1766 ): string {
1767 // Phase 2 AST optimization: Skip processing of ConditionalTerm
1768 // if only one ConditionalFactor is defined
1769 if (! ($condTerm instanceof AST\ConditionalTerm)) {
1770 return $this->walkConditionalFactor($condTerm);
1771 }
1772
1773 return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors));
1774 }
1775
1776 /**
1777 * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL.
1778 */
1779 public function walkConditionalFactor(
1780 AST\ConditionalFactor|AST\ConditionalPrimary $factor,
1781 ): string {
1782 // Phase 2 AST optimization: Skip processing of ConditionalFactor
1783 // if only one ConditionalPrimary is defined
1784 return ! ($factor instanceof AST\ConditionalFactor)
1785 ? $this->walkConditionalPrimary($factor)
1786 : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary);
1787 }
1788
1789 /**
1790 * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL.
1791 */
1792 public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string
1793 {
1794 if ($primary->isSimpleConditionalExpression()) {
1795 return $primary->simpleConditionalExpression->dispatch($this);
1796 }
1797
1798 if ($primary->isConditionalExpression()) {
1799 $condExpr = $primary->conditionalExpression;
1800
1801 return '(' . $this->walkConditionalExpression($condExpr) . ')';
1802 }
1803
1804 throw new LogicException('Unexpected state of ConditionalPrimary node.');
1805 }
1806
1807 /**
1808 * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL.
1809 */
1810 public function walkExistsExpression(AST\ExistsExpression $existsExpr): string
1811 {
1812 $sql = $existsExpr->not ? 'NOT ' : '';
1813
1814 $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')';
1815
1816 return $sql;
1817 }
1818
1819 /**
1820 * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL.
1821 */
1822 public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string
1823 {
1824 $sql = $collMemberExpr->not ? 'NOT ' : '';
1825 $sql .= 'EXISTS (SELECT 1 FROM ';
1826
1827 $entityExpr = $collMemberExpr->entityExpression;
1828 $collPathExpr = $collMemberExpr->collectionValuedPathExpression;
1829 assert($collPathExpr->field !== null);
1830
1831 $fieldName = $collPathExpr->field;
1832 $dqlAlias = $collPathExpr->identificationVariable;
1833
1834 $class = $this->getMetadataForDqlAlias($dqlAlias);
1835
1836 switch (true) {
1837 // InputParameter
1838 case $entityExpr instanceof AST\InputParameter:
1839 $dqlParamKey = $entityExpr->name;
1840 $entitySql = '?';
1841 break;
1842
1843 // SingleValuedAssociationPathExpression | IdentificationVariable
1844 case $entityExpr instanceof AST\PathExpression:
1845 $entitySql = $this->walkPathExpression($entityExpr);
1846 break;
1847
1848 default:
1849 throw new BadMethodCallException('Not implemented');
1850 }
1851
1852 $assoc = $class->associationMappings[$fieldName];
1853
1854 if ($assoc->isOneToMany()) {
1855 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
1856 $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName());
1857 $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1858
1859 $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE ';
1860
1861 $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy];
1862 assert($owningAssoc->isManyToOne());
1863 $sqlParts = [];
1864
1865 foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) {
1866 $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform);
1867
1868 $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
1869 }
1870
1871 foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) {
1872 if (isset($dqlParamKey)) {
1873 $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1874 }
1875
1876 $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql;
1877 }
1878
1879 $sql .= implode(' AND ', $sqlParts);
1880 } else { // many-to-many
1881 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
1882
1883 $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc);
1884 assert($owningAssoc->isManyToManyOwningSide());
1885 $joinTable = $owningAssoc->joinTable;
1886
1887 // SQL table aliases
1888 $joinTableAlias = $this->getSQLTableAlias($joinTable->name);
1889 $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1890
1891 $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE ';
1892
1893 $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns;
1894 $sqlParts = [];
1895
1896 foreach ($joinColumns as $joinColumn) {
1897 $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform);
1898
1899 $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn;
1900 }
1901
1902 $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns;
1903
1904 foreach ($joinColumns as $joinColumn) {
1905 if (isset($dqlParamKey)) {
1906 $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1907 }
1908
1909 $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')';
1910 }
1911
1912 $sql .= implode(' AND ', $sqlParts);
1913 }
1914
1915 return $sql . ')';
1916 }
1917
1918 /**
1919 * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL.
1920 */
1921 public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string
1922 {
1923 $sizeFunc = new AST\Functions\SizeFunction('size');
1924 $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression;
1925
1926 return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0');
1927 }
1928
1929 /**
1930 * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL.
1931 */
1932 public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string
1933 {
1934 $expression = $nullCompExpr->expression;
1935 $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL';
1936
1937 // Handle ResultVariable
1938 if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) {
1939 return $this->walkResultVariable($expression) . $comparison;
1940 }
1941
1942 // Handle InputParameter mapping inclusion to ParserResult
1943 if ($expression instanceof AST\InputParameter) {
1944 return $this->walkInputParameter($expression) . $comparison;
1945 }
1946
1947 assert(! is_string($expression));
1948
1949 return $expression->dispatch($this) . $comparison;
1950 }
1951
1952 /**
1953 * Walks down an InExpression AST node, thereby generating the appropriate SQL.
1954 */
1955 public function walkInListExpression(AST\InListExpression $inExpr): string
1956 {
1957 return $this->walkArithmeticExpression($inExpr->expression)
1958 . ($inExpr->not ? ' NOT' : '') . ' IN ('
1959 . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals))
1960 . ')';
1961 }
1962
1963 /**
1964 * Walks down an InExpression AST node, thereby generating the appropriate SQL.
1965 */
1966 public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string
1967 {
1968 return $this->walkArithmeticExpression($inExpr->expression)
1969 . ($inExpr->not ? ' NOT' : '') . ' IN ('
1970 . $this->walkSubselect($inExpr->subselect)
1971 . ')';
1972 }
1973
1974 /**
1975 * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL.
1976 *
1977 * @throws QueryException
1978 */
1979 public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string
1980 {
1981 $sql = '';
1982
1983 $dqlAlias = $instanceOfExpr->identificationVariable;
1984 $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias);
1985
1986 if ($class->discriminatorColumn) {
1987 $discrClass = $this->em->getClassMetadata($class->rootEntityName);
1988 }
1989
1990 if ($this->useSqlTableAliases) {
1991 $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.';
1992 }
1993
1994 $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN ');
1995 $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr);
1996
1997 return $sql;
1998 }
1999
2000 public function walkInParameter(mixed $inParam): string
2001 {
2002 return $inParam instanceof AST\InputParameter
2003 ? $this->walkInputParameter($inParam)
2004 : $this->walkArithmeticExpression($inParam);
2005 }
2006
2007 /**
2008 * Walks down a literal that represents an AST node, thereby generating the appropriate SQL.
2009 */
2010 public function walkLiteral(AST\Literal $literal): string
2011 {
2012 return match ($literal->type) {
2013 AST\Literal::STRING => $this->conn->quote($literal->value),
2014 AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'),
2015 AST\Literal::NUMERIC => (string) $literal->value,
2016 default => throw QueryException::invalidLiteral($literal),
2017 };
2018 }
2019
2020 /**
2021 * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL.
2022 */
2023 public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string
2024 {
2025 $sql = $this->walkArithmeticExpression($betweenExpr->expression);
2026
2027 if ($betweenExpr->not) {
2028 $sql .= ' NOT';
2029 }
2030
2031 $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression)
2032 . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression);
2033
2034 return $sql;
2035 }
2036
2037 /**
2038 * Walks down a LikeExpression AST node, thereby generating the appropriate SQL.
2039 */
2040 public function walkLikeExpression(AST\LikeExpression $likeExpr): string
2041 {
2042 $stringExpr = $likeExpr->stringExpression;
2043 if (is_string($stringExpr)) {
2044 if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) {
2045 throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr));
2046 }
2047
2048 $leftExpr = $this->walkResultVariable($stringExpr);
2049 } else {
2050 $leftExpr = $stringExpr->dispatch($this);
2051 }
2052
2053 $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE ';
2054
2055 if ($likeExpr->stringPattern instanceof AST\InputParameter) {
2056 $sql .= $this->walkInputParameter($likeExpr->stringPattern);
2057 } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) {
2058 $sql .= $this->walkFunction($likeExpr->stringPattern);
2059 } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) {
2060 $sql .= $this->walkPathExpression($likeExpr->stringPattern);
2061 } else {
2062 $sql .= $this->walkLiteral($likeExpr->stringPattern);
2063 }
2064
2065 if ($likeExpr->escapeChar) {
2066 $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar);
2067 }
2068
2069 return $sql;
2070 }
2071
2072 /**
2073 * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL.
2074 */
2075 public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string
2076 {
2077 return $this->walkPathExpression($stateFieldPathExpression);
2078 }
2079
2080 /**
2081 * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL.
2082 */
2083 public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string
2084 {
2085 $leftExpr = $compExpr->leftExpression;
2086 $rightExpr = $compExpr->rightExpression;
2087 $sql = '';
2088
2089 $sql .= $leftExpr instanceof AST\Node
2090 ? $leftExpr->dispatch($this)
2091 : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr));
2092
2093 $sql .= ' ' . $compExpr->operator . ' ';
2094
2095 $sql .= $rightExpr instanceof AST\Node
2096 ? $rightExpr->dispatch($this)
2097 : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr));
2098
2099 return $sql;
2100 }
2101
2102 /**
2103 * Walks down an InputParameter AST node, thereby generating the appropriate SQL.
2104 */
2105 public function walkInputParameter(AST\InputParameter $inputParam): string
2106 {
2107 $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++);
2108
2109 $parameter = $this->query->getParameter($inputParam->name);
2110
2111 if ($parameter) {
2112 $type = $parameter->getType();
2113 if (is_string($type) && Type::hasType($type)) {
2114 return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform);
2115 }
2116 }
2117
2118 return '?';
2119 }
2120
2121 /**
2122 * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL.
2123 */
2124 public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string
2125 {
2126 return $arithmeticExpr->isSimpleArithmeticExpression()
2127 ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression)
2128 : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')';
2129 }
2130
2131 /**
2132 * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL.
2133 */
2134 public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string
2135 {
2136 if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) {
2137 return $this->walkArithmeticTerm($simpleArithmeticExpr);
2138 }
2139
2140 return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms));
2141 }
2142
2143 /**
2144 * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL.
2145 */
2146 public function walkArithmeticTerm(AST\Node|string $term): string
2147 {
2148 if (is_string($term)) {
2149 return isset($this->queryComponents[$term])
2150 ? $this->walkResultVariable($this->queryComponents[$term]['token']->value)
2151 : $term;
2152 }
2153
2154 // Phase 2 AST optimization: Skip processing of ArithmeticTerm
2155 // if only one ArithmeticFactor is defined
2156 if (! ($term instanceof AST\ArithmeticTerm)) {
2157 return $this->walkArithmeticFactor($term);
2158 }
2159
2160 return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors));
2161 }
2162
2163 /**
2164 * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL.
2165 */
2166 public function walkArithmeticFactor(AST\Node|string $factor): string
2167 {
2168 if (is_string($factor)) {
2169 return isset($this->queryComponents[$factor])
2170 ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value)
2171 : $factor;
2172 }
2173
2174 // Phase 2 AST optimization: Skip processing of ArithmeticFactor
2175 // if only one ArithmeticPrimary is defined
2176 if (! ($factor instanceof AST\ArithmeticFactor)) {
2177 return $this->walkArithmeticPrimary($factor);
2178 }
2179
2180 $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : '');
2181
2182 return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary);
2183 }
2184
2185 /**
2186 * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL.
2187 */
2188 public function walkArithmeticPrimary(AST\Node|string $primary): string
2189 {
2190 if ($primary instanceof AST\SimpleArithmeticExpression) {
2191 return '(' . $this->walkSimpleArithmeticExpression($primary) . ')';
2192 }
2193
2194 if ($primary instanceof AST\Node) {
2195 return $primary->dispatch($this);
2196 }
2197
2198 return $this->walkEntityIdentificationVariable($primary);
2199 }
2200
2201 /**
2202 * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL.
2203 */
2204 public function walkStringPrimary(AST\Node|string $stringPrimary): string
2205 {
2206 return is_string($stringPrimary)
2207 ? $this->conn->quote($stringPrimary)
2208 : $stringPrimary->dispatch($this);
2209 }
2210
2211 /**
2212 * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL.
2213 */
2214 public function walkResultVariable(string $resultVariable): string
2215 {
2216 if (! isset($this->scalarResultAliasMap[$resultVariable])) {
2217 throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable));
2218 }
2219
2220 $resultAlias = $this->scalarResultAliasMap[$resultVariable];
2221
2222 if (is_array($resultAlias)) {
2223 return implode(', ', $resultAlias);
2224 }
2225
2226 return $resultAlias;
2227 }
2228
2229 /**
2230 * @return string The list in parentheses of valid child discriminators from the given class
2231 *
2232 * @throws QueryException
2233 */
2234 private function getChildDiscriminatorsFromClassMetadata(
2235 ClassMetadata $rootClass,
2236 AST\InstanceOfExpression $instanceOfExpr,
2237 ): string {
2238 $sqlParameterList = [];
2239 $discriminators = [];
2240 foreach ($instanceOfExpr->value as $parameter) {
2241 if ($parameter instanceof AST\InputParameter) {
2242 $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name;
2243 $sqlParameterList[] = $this->walkInParameter($parameter);
2244 continue;
2245 }
2246
2247 $metadata = $this->em->getClassMetadata($parameter);
2248
2249 if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) {
2250 throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name);
2251 }
2252
2253 $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em);
2254 }
2255
2256 foreach (array_keys($discriminators) as $discriminatorValue) {
2257 $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue)
2258 ? $discriminatorValue
2259 : $this->conn->quote((string) $discriminatorValue);
2260 }
2261
2262 return '(' . implode(', ', $sqlParameterList) . ')';
2263 }
2264}
diff --git a/vendor/doctrine/orm/src/Query/TokenType.php b/vendor/doctrine/orm/src/Query/TokenType.php
new file mode 100644
index 0000000..e745e4a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/TokenType.php
@@ -0,0 +1,91 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7enum TokenType: int
8{
9 // All tokens that are not valid identifiers must be < 100
10 case T_NONE = 1;
11 case T_INTEGER = 2;
12 case T_STRING = 3;
13 case T_INPUT_PARAMETER = 4;
14 case T_FLOAT = 5;
15 case T_CLOSE_PARENTHESIS = 6;
16 case T_OPEN_PARENTHESIS = 7;
17 case T_COMMA = 8;
18 case T_DIVIDE = 9;
19 case T_DOT = 10;
20 case T_EQUALS = 11;
21 case T_GREATER_THAN = 12;
22 case T_LOWER_THAN = 13;
23 case T_MINUS = 14;
24 case T_MULTIPLY = 15;
25 case T_NEGATE = 16;
26 case T_PLUS = 17;
27 case T_OPEN_CURLY_BRACE = 18;
28 case T_CLOSE_CURLY_BRACE = 19;
29
30 // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100
31 case T_FULLY_QUALIFIED_NAME = 101;
32 case T_IDENTIFIER = 102;
33
34 // All keyword tokens should be >= 200
35 case T_ALL = 200;
36 case T_AND = 201;
37 case T_ANY = 202;
38 case T_AS = 203;
39 case T_ASC = 204;
40 case T_AVG = 205;
41 case T_BETWEEN = 206;
42 case T_BOTH = 207;
43 case T_BY = 208;
44 case T_CASE = 209;
45 case T_COALESCE = 210;
46 case T_COUNT = 211;
47 case T_DELETE = 212;
48 case T_DESC = 213;
49 case T_DISTINCT = 214;
50 case T_ELSE = 215;
51 case T_EMPTY = 216;
52 case T_END = 217;
53 case T_ESCAPE = 218;
54 case T_EXISTS = 219;
55 case T_FALSE = 220;
56 case T_FROM = 221;
57 case T_GROUP = 222;
58 case T_HAVING = 223;
59 case T_HIDDEN = 224;
60 case T_IN = 225;
61 case T_INDEX = 226;
62 case T_INNER = 227;
63 case T_INSTANCE = 228;
64 case T_IS = 229;
65 case T_JOIN = 230;
66 case T_LEADING = 231;
67 case T_LEFT = 232;
68 case T_LIKE = 233;
69 case T_MAX = 234;
70 case T_MEMBER = 235;
71 case T_MIN = 236;
72 case T_NEW = 237;
73 case T_NOT = 238;
74 case T_NULL = 239;
75 case T_NULLIF = 240;
76 case T_OF = 241;
77 case T_OR = 242;
78 case T_ORDER = 243;
79 case T_OUTER = 244;
80 case T_SELECT = 246;
81 case T_SET = 247;
82 case T_SOME = 248;
83 case T_SUM = 249;
84 case T_THEN = 250;
85 case T_TRAILING = 251;
86 case T_TRUE = 252;
87 case T_UPDATE = 253;
88 case T_WHEN = 254;
89 case T_WHERE = 255;
90 case T_WITH = 256;
91}
diff --git a/vendor/doctrine/orm/src/Query/TreeWalker.php b/vendor/doctrine/orm/src/Query/TreeWalker.php
new file mode 100644
index 0000000..6c21577
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/TreeWalker.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\AbstractQuery;
8
9/**
10 * Interface for walkers of DQL ASTs (abstract syntax trees).
11 *
12 * @psalm-import-type QueryComponent from Parser
13 */
14interface TreeWalker
15{
16 /**
17 * Initializes TreeWalker with important information about the ASTs to be walked.
18 *
19 * @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table).
20 */
21 public function __construct(AbstractQuery $query, ParserResult $parserResult, array $queryComponents);
22
23 /**
24 * Returns internal queryComponents array.
25 *
26 * @psalm-return array<string, QueryComponent>
27 */
28 public function getQueryComponents(): array;
29
30 /**
31 * Walks down a SelectStatement AST node.
32 */
33 public function walkSelectStatement(AST\SelectStatement $selectStatement): void;
34
35 /**
36 * Walks down an UpdateStatement AST node.
37 */
38 public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void;
39
40 /**
41 * Walks down a DeleteStatement AST node.
42 */
43 public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void;
44}
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php
new file mode 100644
index 0000000..a7948db
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php
@@ -0,0 +1,90 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\AbstractQuery;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use LogicException;
10
11use function array_diff;
12use function array_keys;
13use function sprintf;
14
15/**
16 * An adapter implementation of the TreeWalker interface. The methods in this class
17 * are empty. This class exists as convenience for creating tree walkers.
18 *
19 * @psalm-import-type QueryComponent from Parser
20 */
21abstract class TreeWalkerAdapter implements TreeWalker
22{
23 /**
24 * {@inheritDoc}
25 */
26 public function __construct(
27 private readonly AbstractQuery $query,
28 private readonly ParserResult $parserResult,
29 private array $queryComponents,
30 ) {
31 }
32
33 /**
34 * {@inheritDoc}
35 */
36 public function getQueryComponents(): array
37 {
38 return $this->queryComponents;
39 }
40
41 public function walkSelectStatement(AST\SelectStatement $selectStatement): void
42 {
43 }
44
45 public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void
46 {
47 }
48
49 public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void
50 {
51 }
52
53 /**
54 * Sets or overrides a query component for a given dql alias.
55 *
56 * @psalm-param QueryComponent $queryComponent
57 */
58 protected function setQueryComponent(string $dqlAlias, array $queryComponent): void
59 {
60 $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token'];
61
62 if (array_diff($requiredKeys, array_keys($queryComponent))) {
63 throw QueryException::invalidQueryComponent($dqlAlias);
64 }
65
66 $this->queryComponents[$dqlAlias] = $queryComponent;
67 }
68
69 /**
70 * Retrieves the Query Instance responsible for the current walkers execution.
71 */
72 protected function _getQuery(): AbstractQuery
73 {
74 return $this->query;
75 }
76
77 /**
78 * Retrieves the ParserResult.
79 */
80 protected function _getParserResult(): ParserResult
81 {
82 return $this->parserResult;
83 }
84
85 protected function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata
86 {
87 return $this->queryComponents[$dqlAlias]['metadata']
88 ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias));
89 }
90}
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerChain.php b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php
new file mode 100644
index 0000000..7bb3051
--- /dev/null
+++ b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\ORM\AbstractQuery;
8use Generator;
9
10/**
11 * Represents a chain of tree walkers that modify an AST and finally emit output.
12 * Only the last walker in the chain can emit output. Any previous walkers can modify
13 * the AST to influence the final output produced by the last walker.
14 *
15 * @psalm-import-type QueryComponent from Parser
16 */
17class TreeWalkerChain implements TreeWalker
18{
19 /**
20 * The tree walkers.
21 *
22 * @var string[]
23 * @psalm-var list<class-string<TreeWalker>>
24 */
25 private array $walkers = [];
26
27 /**
28 * {@inheritDoc}
29 */
30 public function __construct(
31 private readonly AbstractQuery $query,
32 private readonly ParserResult $parserResult,
33 private array $queryComponents,
34 ) {
35 }
36
37 /**
38 * Returns the internal queryComponents array.
39 *
40 * {@inheritDoc}
41 */
42 public function getQueryComponents(): array
43 {
44 return $this->queryComponents;
45 }
46
47 /**
48 * Adds a tree walker to the chain.
49 *
50 * @param string $walkerClass The class of the walker to instantiate.
51 * @psalm-param class-string<TreeWalker> $walkerClass
52 */
53 public function addTreeWalker(string $walkerClass): void
54 {
55 $this->walkers[] = $walkerClass;
56 }
57
58 public function walkSelectStatement(AST\SelectStatement $selectStatement): void
59 {
60 foreach ($this->getWalkers() as $walker) {
61 $walker->walkSelectStatement($selectStatement);
62
63 $this->queryComponents = $walker->getQueryComponents();
64 }
65 }
66
67 public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void
68 {
69 foreach ($this->getWalkers() as $walker) {
70 $walker->walkUpdateStatement($updateStatement);
71 }
72 }
73
74 public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void
75 {
76 foreach ($this->getWalkers() as $walker) {
77 $walker->walkDeleteStatement($deleteStatement);
78 }
79 }
80
81 /** @psalm-return Generator<int, TreeWalker> */
82 private function getWalkers(): Generator
83 {
84 foreach ($this->walkers as $walkerClass) {
85 yield new $walkerClass($this->query, $this->parserResult, $this->queryComponents);
86 }
87 }
88}
diff --git a/vendor/doctrine/orm/src/QueryBuilder.php b/vendor/doctrine/orm/src/QueryBuilder.php
new file mode 100644
index 0000000..a6a39a9
--- /dev/null
+++ b/vendor/doctrine/orm/src/QueryBuilder.php
@@ -0,0 +1,1375 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\Common\Collections\ArrayCollection;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\DBAL\ArrayParameterType;
10use Doctrine\DBAL\ParameterType;
11use Doctrine\ORM\Internal\NoUnknownNamedArguments;
12use Doctrine\ORM\Internal\QueryType;
13use Doctrine\ORM\Query\Expr;
14use Doctrine\ORM\Query\Parameter;
15use Doctrine\ORM\Query\QueryExpressionVisitor;
16use InvalidArgumentException;
17use RuntimeException;
18use Stringable;
19
20use function array_keys;
21use function array_unshift;
22use function assert;
23use function count;
24use function implode;
25use function in_array;
26use function is_array;
27use function is_numeric;
28use function is_object;
29use function is_string;
30use function key;
31use function reset;
32use function sprintf;
33use function str_starts_with;
34use function strpos;
35use function strrpos;
36use function substr;
37
38/**
39 * This class is responsible for building DQL query strings via an object oriented
40 * PHP interface.
41 */
42class QueryBuilder implements Stringable
43{
44 use NoUnknownNamedArguments;
45
46 /**
47 * The array of DQL parts collected.
48 *
49 * @psalm-var array<string, mixed>
50 */
51 private array $dqlParts = [
52 'distinct' => false,
53 'select' => [],
54 'from' => [],
55 'join' => [],
56 'set' => [],
57 'where' => null,
58 'groupBy' => [],
59 'having' => null,
60 'orderBy' => [],
61 ];
62
63 private QueryType $type = QueryType::Select;
64
65 /**
66 * The complete DQL string for this query.
67 */
68 private string|null $dql = null;
69
70 /**
71 * The query parameters.
72 *
73 * @psalm-var ArrayCollection<int, Parameter>
74 */
75 private ArrayCollection $parameters;
76
77 /**
78 * The index of the first result to retrieve.
79 */
80 private int $firstResult = 0;
81
82 /**
83 * The maximum number of results to retrieve.
84 */
85 private int|null $maxResults = null;
86
87 /**
88 * Keeps root entity alias names for join entities.
89 *
90 * @psalm-var array<string, string>
91 */
92 private array $joinRootAliases = [];
93
94 /**
95 * Whether to use second level cache, if available.
96 */
97 protected bool $cacheable = false;
98
99 /**
100 * Second level cache region name.
101 */
102 protected string|null $cacheRegion = null;
103
104 /**
105 * Second level query cache mode.
106 *
107 * @psalm-var Cache::MODE_*|null
108 */
109 protected int|null $cacheMode = null;
110
111 protected int $lifetime = 0;
112
113 /**
114 * Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
115 *
116 * @param EntityManagerInterface $em The EntityManager to use.
117 */
118 public function __construct(
119 private readonly EntityManagerInterface $em,
120 ) {
121 $this->parameters = new ArrayCollection();
122 }
123
124 /**
125 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
126 * This producer method is intended for convenient inline usage. Example:
127 *
128 * <code>
129 * $qb = $em->createQueryBuilder();
130 * $qb
131 * ->select('u')
132 * ->from('User', 'u')
133 * ->where($qb->expr()->eq('u.id', 1));
134 * </code>
135 *
136 * For more complex expression construction, consider storing the expression
137 * builder object in a local variable.
138 */
139 public function expr(): Expr
140 {
141 return $this->em->getExpressionBuilder();
142 }
143
144 /**
145 * Enable/disable second level query (result) caching for this query.
146 *
147 * @return $this
148 */
149 public function setCacheable(bool $cacheable): static
150 {
151 $this->cacheable = $cacheable;
152
153 return $this;
154 }
155
156 /**
157 * Are the query results enabled for second level cache?
158 */
159 public function isCacheable(): bool
160 {
161 return $this->cacheable;
162 }
163
164 /** @return $this */
165 public function setCacheRegion(string $cacheRegion): static
166 {
167 $this->cacheRegion = $cacheRegion;
168
169 return $this;
170 }
171
172 /**
173 * Obtain the name of the second level query cache region in which query results will be stored
174 *
175 * @return string|null The cache region name; NULL indicates the default region.
176 */
177 public function getCacheRegion(): string|null
178 {
179 return $this->cacheRegion;
180 }
181
182 public function getLifetime(): int
183 {
184 return $this->lifetime;
185 }
186
187 /**
188 * Sets the life-time for this query into second level cache.
189 *
190 * @return $this
191 */
192 public function setLifetime(int $lifetime): static
193 {
194 $this->lifetime = $lifetime;
195
196 return $this;
197 }
198
199 /** @psalm-return Cache::MODE_*|null */
200 public function getCacheMode(): int|null
201 {
202 return $this->cacheMode;
203 }
204
205 /**
206 * @psalm-param Cache::MODE_* $cacheMode
207 *
208 * @return $this
209 */
210 public function setCacheMode(int $cacheMode): static
211 {
212 $this->cacheMode = $cacheMode;
213
214 return $this;
215 }
216
217 /**
218 * Gets the associated EntityManager for this query builder.
219 */
220 public function getEntityManager(): EntityManagerInterface
221 {
222 return $this->em;
223 }
224
225 /**
226 * Gets the complete DQL string formed by the current specifications of this QueryBuilder.
227 *
228 * <code>
229 * $qb = $em->createQueryBuilder()
230 * ->select('u')
231 * ->from('User', 'u');
232 * echo $qb->getDql(); // SELECT u FROM User u
233 * </code>
234 */
235 public function getDQL(): string
236 {
237 return $this->dql ??= match ($this->type) {
238 QueryType::Select => $this->getDQLForSelect(),
239 QueryType::Delete => $this->getDQLForDelete(),
240 QueryType::Update => $this->getDQLForUpdate(),
241 };
242 }
243
244 /**
245 * Constructs a Query instance from the current specifications of the builder.
246 *
247 * <code>
248 * $qb = $em->createQueryBuilder()
249 * ->select('u')
250 * ->from('User', 'u');
251 * $q = $qb->getQuery();
252 * $results = $q->execute();
253 * </code>
254 */
255 public function getQuery(): Query
256 {
257 $parameters = clone $this->parameters;
258 $query = $this->em->createQuery($this->getDQL())
259 ->setParameters($parameters)
260 ->setFirstResult($this->firstResult)
261 ->setMaxResults($this->maxResults);
262
263 if ($this->lifetime) {
264 $query->setLifetime($this->lifetime);
265 }
266
267 if ($this->cacheMode) {
268 $query->setCacheMode($this->cacheMode);
269 }
270
271 if ($this->cacheable) {
272 $query->setCacheable($this->cacheable);
273 }
274
275 if ($this->cacheRegion) {
276 $query->setCacheRegion($this->cacheRegion);
277 }
278
279 return $query;
280 }
281
282 /**
283 * Finds the root entity alias of the joined entity.
284 *
285 * @param string $alias The alias of the new join entity
286 * @param string $parentAlias The parent entity alias of the join relationship
287 */
288 private function findRootAlias(string $alias, string $parentAlias): string
289 {
290 if (in_array($parentAlias, $this->getRootAliases(), true)) {
291 $rootAlias = $parentAlias;
292 } elseif (isset($this->joinRootAliases[$parentAlias])) {
293 $rootAlias = $this->joinRootAliases[$parentAlias];
294 } else {
295 // Should never happen with correct joining order. Might be
296 // thoughtful to throw exception instead.
297 $rootAlias = $this->getRootAlias();
298 }
299
300 $this->joinRootAliases[$alias] = $rootAlias;
301
302 return $rootAlias;
303 }
304
305 /**
306 * Gets the FIRST root alias of the query. This is the first entity alias involved
307 * in the construction of the query.
308 *
309 * <code>
310 * $qb = $em->createQueryBuilder()
311 * ->select('u')
312 * ->from('User', 'u');
313 *
314 * echo $qb->getRootAlias(); // u
315 * </code>
316 *
317 * @deprecated Please use $qb->getRootAliases() instead.
318 *
319 * @throws RuntimeException
320 */
321 public function getRootAlias(): string
322 {
323 $aliases = $this->getRootAliases();
324
325 if (! isset($aliases[0])) {
326 throw new RuntimeException('No alias was set before invoking getRootAlias().');
327 }
328
329 return $aliases[0];
330 }
331
332 /**
333 * Gets the root aliases of the query. This is the entity aliases involved
334 * in the construction of the query.
335 *
336 * <code>
337 * $qb = $em->createQueryBuilder()
338 * ->select('u')
339 * ->from('User', 'u');
340 *
341 * $qb->getRootAliases(); // array('u')
342 * </code>
343 *
344 * @return string[]
345 * @psalm-return list<string>
346 */
347 public function getRootAliases(): array
348 {
349 $aliases = [];
350
351 foreach ($this->dqlParts['from'] as &$fromClause) {
352 if (is_string($fromClause)) {
353 $spacePos = strrpos($fromClause, ' ');
354
355 /** @psalm-var class-string $from */
356 $from = substr($fromClause, 0, $spacePos);
357 $alias = substr($fromClause, $spacePos + 1);
358
359 $fromClause = new Query\Expr\From($from, $alias);
360 }
361
362 $aliases[] = $fromClause->getAlias();
363 }
364
365 return $aliases;
366 }
367
368 /**
369 * Gets all the aliases that have been used in the query.
370 * Including all select root aliases and join aliases
371 *
372 * <code>
373 * $qb = $em->createQueryBuilder()
374 * ->select('u')
375 * ->from('User', 'u')
376 * ->join('u.articles','a');
377 *
378 * $qb->getAllAliases(); // array('u','a')
379 * </code>
380 *
381 * @return string[]
382 * @psalm-return list<string>
383 */
384 public function getAllAliases(): array
385 {
386 return [...$this->getRootAliases(), ...array_keys($this->joinRootAliases)];
387 }
388
389 /**
390 * Gets the root entities of the query. This is the entity classes involved
391 * in the construction of the query.
392 *
393 * <code>
394 * $qb = $em->createQueryBuilder()
395 * ->select('u')
396 * ->from('User', 'u');
397 *
398 * $qb->getRootEntities(); // array('User')
399 * </code>
400 *
401 * @return string[]
402 * @psalm-return list<class-string>
403 */
404 public function getRootEntities(): array
405 {
406 $entities = [];
407
408 foreach ($this->dqlParts['from'] as &$fromClause) {
409 if (is_string($fromClause)) {
410 $spacePos = strrpos($fromClause, ' ');
411
412 /** @psalm-var class-string $from */
413 $from = substr($fromClause, 0, $spacePos);
414 $alias = substr($fromClause, $spacePos + 1);
415
416 $fromClause = new Query\Expr\From($from, $alias);
417 }
418
419 $entities[] = $fromClause->getFrom();
420 }
421
422 return $entities;
423 }
424
425 /**
426 * Sets a query parameter for the query being constructed.
427 *
428 * <code>
429 * $qb = $em->createQueryBuilder()
430 * ->select('u')
431 * ->from('User', 'u')
432 * ->where('u.id = :user_id')
433 * ->setParameter('user_id', 1);
434 * </code>
435 *
436 * @param string|int $key The parameter position or name.
437 * @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
438 *
439 * @return $this
440 */
441 public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
442 {
443 $existingParameter = $this->getParameter($key);
444
445 if ($existingParameter !== null) {
446 $existingParameter->setValue($value, $type);
447
448 return $this;
449 }
450
451 $this->parameters->add(new Parameter($key, $value, $type));
452
453 return $this;
454 }
455
456 /**
457 * Sets a collection of query parameters for the query being constructed.
458 *
459 * <code>
460 * $qb = $em->createQueryBuilder()
461 * ->select('u')
462 * ->from('User', 'u')
463 * ->where('u.id = :user_id1 OR u.id = :user_id2')
464 * ->setParameters(new ArrayCollection(array(
465 * new Parameter('user_id1', 1),
466 * new Parameter('user_id2', 2)
467 * )));
468 * </code>
469 *
470 * @psalm-param ArrayCollection<int, Parameter> $parameters
471 *
472 * @return $this
473 */
474 public function setParameters(ArrayCollection $parameters): static
475 {
476 $this->parameters = $parameters;
477
478 return $this;
479 }
480
481 /**
482 * Gets all defined query parameters for the query being constructed.
483 *
484 * @psalm-return ArrayCollection<int, Parameter>
485 */
486 public function getParameters(): ArrayCollection
487 {
488 return $this->parameters;
489 }
490
491 /**
492 * Gets a (previously set) query parameter of the query being constructed.
493 */
494 public function getParameter(string|int $key): Parameter|null
495 {
496 $key = Parameter::normalizeName($key);
497
498 $filteredParameters = $this->parameters->filter(
499 static fn (Parameter $parameter): bool => $key === $parameter->getName()
500 );
501
502 return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null;
503 }
504
505 /**
506 * Sets the position of the first result to retrieve (the "offset").
507 *
508 * @return $this
509 */
510 public function setFirstResult(int|null $firstResult): static
511 {
512 $this->firstResult = (int) $firstResult;
513
514 return $this;
515 }
516
517 /**
518 * Gets the position of the first result the query object was set to retrieve (the "offset").
519 */
520 public function getFirstResult(): int
521 {
522 return $this->firstResult;
523 }
524
525 /**
526 * Sets the maximum number of results to retrieve (the "limit").
527 *
528 * @return $this
529 */
530 public function setMaxResults(int|null $maxResults): static
531 {
532 $this->maxResults = $maxResults;
533
534 return $this;
535 }
536
537 /**
538 * Gets the maximum number of results the query object was set to retrieve (the "limit").
539 * Returns NULL if {@link setMaxResults} was not applied to this query builder.
540 */
541 public function getMaxResults(): int|null
542 {
543 return $this->maxResults;
544 }
545
546 /**
547 * Either appends to or replaces a single, generic query part.
548 *
549 * The available parts are: 'select', 'from', 'join', 'set', 'where',
550 * 'groupBy', 'having' and 'orderBy'.
551 *
552 * @psalm-param string|object|list<string>|array{join: array<int|string, object>} $dqlPart
553 *
554 * @return $this
555 */
556 public function add(string $dqlPartName, string|object|array $dqlPart, bool $append = false): static
557 {
558 if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) {
559 throw new InvalidArgumentException(
560 "Using \$append = true does not have an effect with 'where' or 'having' " .
561 'parts. See QueryBuilder#andWhere() for an example for correct usage.',
562 );
563 }
564
565 $isMultiple = is_array($this->dqlParts[$dqlPartName])
566 && ! ($dqlPartName === 'join' && ! $append);
567
568 // Allow adding any part retrieved from self::getDQLParts().
569 if (is_array($dqlPart) && $dqlPartName !== 'join') {
570 $dqlPart = reset($dqlPart);
571 }
572
573 // This is introduced for backwards compatibility reasons.
574 // TODO: Remove for 3.0
575 if ($dqlPartName === 'join') {
576 $newDqlPart = [];
577
578 foreach ($dqlPart as $k => $v) {
579 $k = is_numeric($k) ? $this->getRootAlias() : $k;
580
581 $newDqlPart[$k] = $v;
582 }
583
584 $dqlPart = $newDqlPart;
585 }
586
587 if ($append && $isMultiple) {
588 if (is_array($dqlPart)) {
589 $key = key($dqlPart);
590
591 $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key];
592 } else {
593 $this->dqlParts[$dqlPartName][] = $dqlPart;
594 }
595 } else {
596 $this->dqlParts[$dqlPartName] = $isMultiple ? [$dqlPart] : $dqlPart;
597 }
598
599 $this->dql = null;
600
601 return $this;
602 }
603
604 /**
605 * Specifies an item that is to be returned in the query result.
606 * Replaces any previously specified selections, if any.
607 *
608 * <code>
609 * $qb = $em->createQueryBuilder()
610 * ->select('u', 'p')
611 * ->from('User', 'u')
612 * ->leftJoin('u.Phonenumbers', 'p');
613 * </code>
614 *
615 * @return $this
616 */
617 public function select(mixed ...$select): static
618 {
619 self::validateVariadicParameter($select);
620
621 $this->type = QueryType::Select;
622
623 if ($select === []) {
624 return $this;
625 }
626
627 return $this->add('select', new Expr\Select($select), false);
628 }
629
630 /**
631 * Adds a DISTINCT flag to this query.
632 *
633 * <code>
634 * $qb = $em->createQueryBuilder()
635 * ->select('u')
636 * ->distinct()
637 * ->from('User', 'u');
638 * </code>
639 *
640 * @return $this
641 */
642 public function distinct(bool $flag = true): static
643 {
644 if ($this->dqlParts['distinct'] !== $flag) {
645 $this->dqlParts['distinct'] = $flag;
646 $this->dql = null;
647 }
648
649 return $this;
650 }
651
652 /**
653 * Adds an item that is to be returned in the query result.
654 *
655 * <code>
656 * $qb = $em->createQueryBuilder()
657 * ->select('u')
658 * ->addSelect('p')
659 * ->from('User', 'u')
660 * ->leftJoin('u.Phonenumbers', 'p');
661 * </code>
662 *
663 * @return $this
664 */
665 public function addSelect(mixed ...$select): static
666 {
667 self::validateVariadicParameter($select);
668
669 $this->type = QueryType::Select;
670
671 if ($select === []) {
672 return $this;
673 }
674
675 return $this->add('select', new Expr\Select($select), true);
676 }
677
678 /**
679 * Turns the query being built into a bulk delete query that ranges over
680 * a certain entity type.
681 *
682 * <code>
683 * $qb = $em->createQueryBuilder()
684 * ->delete('User', 'u')
685 * ->where('u.id = :user_id')
686 * ->setParameter('user_id', 1);
687 * </code>
688 *
689 * @param class-string|null $delete The class/type whose instances are subject to the deletion.
690 * @param string|null $alias The class/type alias used in the constructed query.
691 *
692 * @return $this
693 */
694 public function delete(string|null $delete = null, string|null $alias = null): static
695 {
696 $this->type = QueryType::Delete;
697
698 if (! $delete) {
699 return $this;
700 }
701
702 if (! $alias) {
703 throw new InvalidArgumentException(sprintf(
704 '%s(): The alias for entity %s must not be omitted.',
705 __METHOD__,
706 $delete,
707 ));
708 }
709
710 return $this->add('from', new Expr\From($delete, $alias));
711 }
712
713 /**
714 * Turns the query being built into a bulk update query that ranges over
715 * a certain entity type.
716 *
717 * <code>
718 * $qb = $em->createQueryBuilder()
719 * ->update('User', 'u')
720 * ->set('u.password', '?1')
721 * ->where('u.id = ?2');
722 * </code>
723 *
724 * @param class-string|null $update The class/type whose instances are subject to the update.
725 * @param string|null $alias The class/type alias used in the constructed query.
726 *
727 * @return $this
728 */
729 public function update(string|null $update = null, string|null $alias = null): static
730 {
731 $this->type = QueryType::Update;
732
733 if (! $update) {
734 return $this;
735 }
736
737 if (! $alias) {
738 throw new InvalidArgumentException(sprintf(
739 '%s(): The alias for entity %s must not be omitted.',
740 __METHOD__,
741 $update,
742 ));
743 }
744
745 return $this->add('from', new Expr\From($update, $alias));
746 }
747
748 /**
749 * Creates and adds a query root corresponding to the entity identified by the given alias,
750 * forming a cartesian product with any existing query roots.
751 *
752 * <code>
753 * $qb = $em->createQueryBuilder()
754 * ->select('u')
755 * ->from('User', 'u');
756 * </code>
757 *
758 * @param class-string $from The class name.
759 * @param string $alias The alias of the class.
760 * @param string|null $indexBy The index for the from.
761 *
762 * @return $this
763 */
764 public function from(string $from, string $alias, string|null $indexBy = null): static
765 {
766 return $this->add('from', new Expr\From($from, $alias, $indexBy), true);
767 }
768
769 /**
770 * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with
771 * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it
772 * setting an index by.
773 *
774 * <code>
775 * $qb = $userRepository->createQueryBuilder('u')
776 * ->indexBy('u', 'u.id');
777 *
778 * // Is equivalent to...
779 *
780 * $qb = $em->createQueryBuilder()
781 * ->select('u')
782 * ->from('User', 'u', 'u.id');
783 * </code>
784 *
785 * @return $this
786 *
787 * @throws Query\QueryException
788 */
789 public function indexBy(string $alias, string $indexBy): static
790 {
791 $rootAliases = $this->getRootAliases();
792
793 if (! in_array($alias, $rootAliases, true)) {
794 throw new Query\QueryException(
795 sprintf('Specified root alias %s must be set before invoking indexBy().', $alias),
796 );
797 }
798
799 foreach ($this->dqlParts['from'] as &$fromClause) {
800 assert($fromClause instanceof Expr\From);
801 if ($fromClause->getAlias() !== $alias) {
802 continue;
803 }
804
805 $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy);
806 }
807
808 return $this;
809 }
810
811 /**
812 * Creates and adds a join over an entity association to the query.
813 *
814 * The entities in the joined association will be fetched as part of the query
815 * result if the alias used for the joined association is placed in the select
816 * expressions.
817 *
818 * <code>
819 * $qb = $em->createQueryBuilder()
820 * ->select('u')
821 * ->from('User', 'u')
822 * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
823 * </code>
824 *
825 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
826 *
827 * @return $this
828 */
829 public function join(
830 string $join,
831 string $alias,
832 string|null $conditionType = null,
833 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
834 string|null $indexBy = null,
835 ): static {
836 return $this->innerJoin($join, $alias, $conditionType, $condition, $indexBy);
837 }
838
839 /**
840 * Creates and adds a join over an entity association to the query.
841 *
842 * The entities in the joined association will be fetched as part of the query
843 * result if the alias used for the joined association is placed in the select
844 * expressions.
845 *
846 * [php]
847 * $qb = $em->createQueryBuilder()
848 * ->select('u')
849 * ->from('User', 'u')
850 * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
851 *
852 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
853 *
854 * @return $this
855 */
856 public function innerJoin(
857 string $join,
858 string $alias,
859 string|null $conditionType = null,
860 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
861 string|null $indexBy = null,
862 ): static {
863 $parentAlias = substr($join, 0, (int) strpos($join, '.'));
864
865 $rootAlias = $this->findRootAlias($alias, $parentAlias);
866
867 $join = new Expr\Join(
868 Expr\Join::INNER_JOIN,
869 $join,
870 $alias,
871 $conditionType,
872 $condition,
873 $indexBy,
874 );
875
876 return $this->add('join', [$rootAlias => $join], true);
877 }
878
879 /**
880 * Creates and adds a left join over an entity association to the query.
881 *
882 * The entities in the joined association will be fetched as part of the query
883 * result if the alias used for the joined association is placed in the select
884 * expressions.
885 *
886 * <code>
887 * $qb = $em->createQueryBuilder()
888 * ->select('u')
889 * ->from('User', 'u')
890 * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1');
891 * </code>
892 *
893 * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType
894 *
895 * @return $this
896 */
897 public function leftJoin(
898 string $join,
899 string $alias,
900 string|null $conditionType = null,
901 string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null,
902 string|null $indexBy = null,
903 ): static {
904 $parentAlias = substr($join, 0, (int) strpos($join, '.'));
905
906 $rootAlias = $this->findRootAlias($alias, $parentAlias);
907
908 $join = new Expr\Join(
909 Expr\Join::LEFT_JOIN,
910 $join,
911 $alias,
912 $conditionType,
913 $condition,
914 $indexBy,
915 );
916
917 return $this->add('join', [$rootAlias => $join], true);
918 }
919
920 /**
921 * Sets a new value for a field in a bulk update query.
922 *
923 * <code>
924 * $qb = $em->createQueryBuilder()
925 * ->update('User', 'u')
926 * ->set('u.password', '?1')
927 * ->where('u.id = ?2');
928 * </code>
929 *
930 * @return $this
931 */
932 public function set(string $key, mixed $value): static
933 {
934 return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true);
935 }
936
937 /**
938 * Specifies one or more restrictions to the query result.
939 * Replaces any previously specified restrictions, if any.
940 *
941 * <code>
942 * $qb = $em->createQueryBuilder()
943 * ->select('u')
944 * ->from('User', 'u')
945 * ->where('u.id = ?');
946 *
947 * // You can optionally programmatically build and/or expressions
948 * $qb = $em->createQueryBuilder();
949 *
950 * $or = $qb->expr()->orX();
951 * $or->add($qb->expr()->eq('u.id', 1));
952 * $or->add($qb->expr()->eq('u.id', 2));
953 *
954 * $qb->update('User', 'u')
955 * ->set('u.password', '?')
956 * ->where($or);
957 * </code>
958 *
959 * @return $this
960 */
961 public function where(mixed ...$predicates): static
962 {
963 self::validateVariadicParameter($predicates);
964
965 if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) {
966 $predicates = new Expr\Andx($predicates);
967 }
968
969 return $this->add('where', $predicates);
970 }
971
972 /**
973 * Adds one or more restrictions to the query results, forming a logical
974 * conjunction with any previously specified restrictions.
975 *
976 * <code>
977 * $qb = $em->createQueryBuilder()
978 * ->select('u')
979 * ->from('User', 'u')
980 * ->where('u.username LIKE ?')
981 * ->andWhere('u.is_active = 1');
982 * </code>
983 *
984 * @see where()
985 *
986 * @return $this
987 */
988 public function andWhere(mixed ...$where): static
989 {
990 self::validateVariadicParameter($where);
991
992 $dql = $this->getDQLPart('where');
993
994 if ($dql instanceof Expr\Andx) {
995 $dql->addMultiple($where);
996 } else {
997 array_unshift($where, $dql);
998 $dql = new Expr\Andx($where);
999 }
1000
1001 return $this->add('where', $dql);
1002 }
1003
1004 /**
1005 * Adds one or more restrictions to the query results, forming a logical
1006 * disjunction with any previously specified restrictions.
1007 *
1008 * <code>
1009 * $qb = $em->createQueryBuilder()
1010 * ->select('u')
1011 * ->from('User', 'u')
1012 * ->where('u.id = 1')
1013 * ->orWhere('u.id = 2');
1014 * </code>
1015 *
1016 * @see where()
1017 *
1018 * @return $this
1019 */
1020 public function orWhere(mixed ...$where): static
1021 {
1022 self::validateVariadicParameter($where);
1023
1024 $dql = $this->getDQLPart('where');
1025
1026 if ($dql instanceof Expr\Orx) {
1027 $dql->addMultiple($where);
1028 } else {
1029 array_unshift($where, $dql);
1030 $dql = new Expr\Orx($where);
1031 }
1032
1033 return $this->add('where', $dql);
1034 }
1035
1036 /**
1037 * Specifies a grouping over the results of the query.
1038 * Replaces any previously specified groupings, if any.
1039 *
1040 * <code>
1041 * $qb = $em->createQueryBuilder()
1042 * ->select('u')
1043 * ->from('User', 'u')
1044 * ->groupBy('u.id');
1045 * </code>
1046 *
1047 * @return $this
1048 */
1049 public function groupBy(string ...$groupBy): static
1050 {
1051 self::validateVariadicParameter($groupBy);
1052
1053 return $this->add('groupBy', new Expr\GroupBy($groupBy));
1054 }
1055
1056 /**
1057 * Adds a grouping expression to the query.
1058 *
1059 * <code>
1060 * $qb = $em->createQueryBuilder()
1061 * ->select('u')
1062 * ->from('User', 'u')
1063 * ->groupBy('u.lastLogin')
1064 * ->addGroupBy('u.createdAt');
1065 * </code>
1066 *
1067 * @return $this
1068 */
1069 public function addGroupBy(string ...$groupBy): static
1070 {
1071 self::validateVariadicParameter($groupBy);
1072
1073 return $this->add('groupBy', new Expr\GroupBy($groupBy), true);
1074 }
1075
1076 /**
1077 * Specifies a restriction over the groups of the query.
1078 * Replaces any previous having restrictions, if any.
1079 *
1080 * @return $this
1081 */
1082 public function having(mixed ...$having): static
1083 {
1084 self::validateVariadicParameter($having);
1085
1086 if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) {
1087 $having = new Expr\Andx($having);
1088 }
1089
1090 return $this->add('having', $having);
1091 }
1092
1093 /**
1094 * Adds a restriction over the groups of the query, forming a logical
1095 * conjunction with any existing having restrictions.
1096 *
1097 * @return $this
1098 */
1099 public function andHaving(mixed ...$having): static
1100 {
1101 self::validateVariadicParameter($having);
1102
1103 $dql = $this->getDQLPart('having');
1104
1105 if ($dql instanceof Expr\Andx) {
1106 $dql->addMultiple($having);
1107 } else {
1108 array_unshift($having, $dql);
1109 $dql = new Expr\Andx($having);
1110 }
1111
1112 return $this->add('having', $dql);
1113 }
1114
1115 /**
1116 * Adds a restriction over the groups of the query, forming a logical
1117 * disjunction with any existing having restrictions.
1118 *
1119 * @return $this
1120 */
1121 public function orHaving(mixed ...$having): static
1122 {
1123 self::validateVariadicParameter($having);
1124
1125 $dql = $this->getDQLPart('having');
1126
1127 if ($dql instanceof Expr\Orx) {
1128 $dql->addMultiple($having);
1129 } else {
1130 array_unshift($having, $dql);
1131 $dql = new Expr\Orx($having);
1132 }
1133
1134 return $this->add('having', $dql);
1135 }
1136
1137 /**
1138 * Specifies an ordering for the query results.
1139 * Replaces any previously specified orderings, if any.
1140 *
1141 * @return $this
1142 */
1143 public function orderBy(string|Expr\OrderBy $sort, string|null $order = null): static
1144 {
1145 $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order);
1146
1147 return $this->add('orderBy', $orderBy);
1148 }
1149
1150 /**
1151 * Adds an ordering to the query results.
1152 *
1153 * @return $this
1154 */
1155 public function addOrderBy(string|Expr\OrderBy $sort, string|null $order = null): static
1156 {
1157 $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order);
1158
1159 return $this->add('orderBy', $orderBy, true);
1160 }
1161
1162 /**
1163 * Adds criteria to the query.
1164 *
1165 * Adds where expressions with AND operator.
1166 * Adds orderings.
1167 * Overrides firstResult and maxResults if they're set.
1168 *
1169 * @return $this
1170 *
1171 * @throws Query\QueryException
1172 */
1173 public function addCriteria(Criteria $criteria): static
1174 {
1175 $allAliases = $this->getAllAliases();
1176 if (! isset($allAliases[0])) {
1177 throw new Query\QueryException('No aliases are set before invoking addCriteria().');
1178 }
1179
1180 $visitor = new QueryExpressionVisitor($this->getAllAliases());
1181
1182 $whereExpression = $criteria->getWhereExpression();
1183 if ($whereExpression) {
1184 $this->andWhere($visitor->dispatch($whereExpression));
1185 foreach ($visitor->getParameters() as $parameter) {
1186 $this->parameters->add($parameter);
1187 }
1188 }
1189
1190 foreach ($criteria->orderings() as $sort => $order) {
1191 $hasValidAlias = false;
1192 foreach ($allAliases as $alias) {
1193 if (str_starts_with($sort . '.', $alias . '.')) {
1194 $hasValidAlias = true;
1195 break;
1196 }
1197 }
1198
1199 if (! $hasValidAlias) {
1200 $sort = $allAliases[0] . '.' . $sort;
1201 }
1202
1203 $this->addOrderBy($sort, $order->value);
1204 }
1205
1206 // Overwrite limits only if they was set in criteria
1207 $firstResult = $criteria->getFirstResult();
1208 if ($firstResult > 0) {
1209 $this->setFirstResult($firstResult);
1210 }
1211
1212 $maxResults = $criteria->getMaxResults();
1213 if ($maxResults !== null) {
1214 $this->setMaxResults($maxResults);
1215 }
1216
1217 return $this;
1218 }
1219
1220 /**
1221 * Gets a query part by its name.
1222 */
1223 public function getDQLPart(string $queryPartName): mixed
1224 {
1225 return $this->dqlParts[$queryPartName];
1226 }
1227
1228 /**
1229 * Gets all query parts.
1230 *
1231 * @psalm-return array<string, mixed> $dqlParts
1232 */
1233 public function getDQLParts(): array
1234 {
1235 return $this->dqlParts;
1236 }
1237
1238 private function getDQLForDelete(): string
1239 {
1240 return 'DELETE'
1241 . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1242 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1243 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1244 }
1245
1246 private function getDQLForUpdate(): string
1247 {
1248 return 'UPDATE'
1249 . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', '])
1250 . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ', 'separator' => ', '])
1251 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1252 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1253 }
1254
1255 private function getDQLForSelect(): string
1256 {
1257 $dql = 'SELECT'
1258 . ($this->dqlParts['distinct'] === true ? ' DISTINCT' : '')
1259 . $this->getReducedDQLQueryPart('select', ['pre' => ' ', 'separator' => ', ']);
1260
1261 $fromParts = $this->getDQLPart('from');
1262 $joinParts = $this->getDQLPart('join');
1263 $fromClauses = [];
1264
1265 // Loop through all FROM clauses
1266 if (! empty($fromParts)) {
1267 $dql .= ' FROM ';
1268
1269 foreach ($fromParts as $from) {
1270 $fromClause = (string) $from;
1271
1272 if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) {
1273 foreach ($joinParts[$from->getAlias()] as $join) {
1274 $fromClause .= ' ' . ((string) $join);
1275 }
1276 }
1277
1278 $fromClauses[] = $fromClause;
1279 }
1280 }
1281
1282 $dql .= implode(', ', $fromClauses)
1283 . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE '])
1284 . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ', 'separator' => ', '])
1285 . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING '])
1286 . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']);
1287
1288 return $dql;
1289 }
1290
1291 /** @psalm-param array<string, mixed> $options */
1292 private function getReducedDQLQueryPart(string $queryPartName, array $options = []): string
1293 {
1294 $queryPart = $this->getDQLPart($queryPartName);
1295
1296 if (empty($queryPart)) {
1297 return $options['empty'] ?? '';
1298 }
1299
1300 return ($options['pre'] ?? '')
1301 . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart)
1302 . ($options['post'] ?? '');
1303 }
1304
1305 /**
1306 * Resets DQL parts.
1307 *
1308 * @param string[]|null $parts
1309 * @psalm-param list<string>|null $parts
1310 *
1311 * @return $this
1312 */
1313 public function resetDQLParts(array|null $parts = null): static
1314 {
1315 if ($parts === null) {
1316 $parts = array_keys($this->dqlParts);
1317 }
1318
1319 foreach ($parts as $part) {
1320 $this->resetDQLPart($part);
1321 }
1322
1323 return $this;
1324 }
1325
1326 /**
1327 * Resets single DQL part.
1328 *
1329 * @return $this
1330 */
1331 public function resetDQLPart(string $part): static
1332 {
1333 $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null;
1334 $this->dql = null;
1335
1336 return $this;
1337 }
1338
1339 /**
1340 * Gets a string representation of this QueryBuilder which corresponds to
1341 * the final DQL query being constructed.
1342 */
1343 public function __toString(): string
1344 {
1345 return $this->getDQL();
1346 }
1347
1348 /**
1349 * Deep clones all expression objects in the DQL parts.
1350 *
1351 * @return void
1352 */
1353 public function __clone()
1354 {
1355 foreach ($this->dqlParts as $part => $elements) {
1356 if (is_array($this->dqlParts[$part])) {
1357 foreach ($this->dqlParts[$part] as $idx => $element) {
1358 if (is_object($element)) {
1359 $this->dqlParts[$part][$idx] = clone $element;
1360 }
1361 }
1362 } elseif (is_object($elements)) {
1363 $this->dqlParts[$part] = clone $elements;
1364 }
1365 }
1366
1367 $parameters = [];
1368
1369 foreach ($this->parameters as $parameter) {
1370 $parameters[] = clone $parameter;
1371 }
1372
1373 $this->parameters = new ArrayCollection($parameters);
1374 }
1375}
diff --git a/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php b/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php
new file mode 100644
index 0000000..5c408fb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php
@@ -0,0 +1,49 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Repository;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\EntityRepository;
9use Doctrine\Persistence\ObjectRepository;
10
11use function spl_object_id;
12
13/**
14 * This factory is used to create default repository objects for entities at runtime.
15 */
16final class DefaultRepositoryFactory implements RepositoryFactory
17{
18 /**
19 * The list of EntityRepository instances.
20 *
21 * @var ObjectRepository[]
22 * @psalm-var array<string, EntityRepository>
23 */
24 private array $repositoryList = [];
25
26 public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository
27 {
28 $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_id($entityManager);
29
30 return $this->repositoryList[$repositoryHash] ??= $this->createRepository($entityManager, $entityName);
31 }
32
33 /**
34 * Create a new repository instance for an entity class.
35 *
36 * @param EntityManagerInterface $entityManager The EntityManager instance.
37 * @param string $entityName The name of the entity.
38 */
39 private function createRepository(
40 EntityManagerInterface $entityManager,
41 string $entityName,
42 ): EntityRepository {
43 $metadata = $entityManager->getClassMetadata($entityName);
44 $repositoryClassName = $metadata->customRepositoryClassName
45 ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
46
47 return new $repositoryClassName($entityManager, $metadata);
48 }
49}
diff --git a/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php b/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php
new file mode 100644
index 0000000..c5dd015
--- /dev/null
+++ b/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Repository\Exception;
6
7use Doctrine\ORM\Exception\RepositoryException;
8use LogicException;
9
10final class InvalidFindByCall extends LogicException implements RepositoryException
11{
12 public static function fromInverseSideUsage(
13 string $entityName,
14 string $associationFieldName,
15 ): self {
16 return new self(
17 "You cannot search for the association field '" . $entityName . '#' . $associationFieldName . "', " .
18 'because it is the inverse side of an association. Find methods only work on owning side associations.',
19 );
20 }
21}
diff --git a/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php b/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php
new file mode 100644
index 0000000..1da49cb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Repository\Exception;
6
7use Doctrine\ORM\Exception\RepositoryException;
8use LogicException;
9
10final class InvalidMagicMethodCall extends LogicException implements RepositoryException
11{
12 public static function becauseFieldNotFoundIn(
13 string $entityName,
14 string $fieldName,
15 string $method,
16 ): self {
17 return new self(
18 "Entity '" . $entityName . "' has no field '" . $fieldName . "'. " .
19 "You can therefore not call '" . $method . "' on the entities' repository.",
20 );
21 }
22
23 public static function onMissingParameter(string $methodName): self
24 {
25 return new self("You need to pass a parameter to '" . $methodName . "'");
26 }
27}
diff --git a/vendor/doctrine/orm/src/Repository/RepositoryFactory.php b/vendor/doctrine/orm/src/Repository/RepositoryFactory.php
new file mode 100644
index 0000000..e066eae
--- /dev/null
+++ b/vendor/doctrine/orm/src/Repository/RepositoryFactory.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Repository;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\EntityRepository;
9
10/**
11 * Interface for entity repository factory.
12 */
13interface RepositoryFactory
14{
15 /**
16 * Gets the repository for an entity class.
17 *
18 * @param EntityManagerInterface $entityManager The EntityManager instance.
19 * @param class-string<T> $entityName The name of the entity.
20 *
21 * @return EntityRepository<T>
22 *
23 * @template T of object
24 */
25 public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository;
26}
diff --git a/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php
new file mode 100644
index 0000000..9203cfe
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
8use Doctrine\ORM\Events;
9use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
10
11use function assert;
12use function ltrim;
13
14/**
15 * Mechanism to programmatically attach entity listeners.
16 */
17class AttachEntityListenersListener
18{
19 /**
20 * @var array<class-string, list<array{
21 * event: Events::*|null,
22 * class: class-string,
23 * method: string|null,
24 * }>>
25 */
26 private array $entityListeners = [];
27
28 /**
29 * Adds an entity listener for a specific entity.
30 *
31 * @param class-string $entityClass The entity to attach the listener.
32 * @param class-string $listenerClass The listener class.
33 * @param Events::*|null $eventName The entity lifecycle event.
34 * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName.
35 */
36 public function addEntityListener(
37 string $entityClass,
38 string $listenerClass,
39 string|null $eventName = null,
40 string|null $listenerCallback = null,
41 ): void {
42 $this->entityListeners[ltrim($entityClass, '\\')][] = [
43 'event' => $eventName,
44 'class' => $listenerClass,
45 'method' => $listenerCallback ?? $eventName,
46 ];
47 }
48
49 /**
50 * Processes event and attach the entity listener.
51 */
52 public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
53 {
54 $metadata = $event->getClassMetadata();
55
56 if (! isset($this->entityListeners[$metadata->name])) {
57 return;
58 }
59
60 foreach ($this->entityListeners[$metadata->name] as $listener) {
61 if ($listener['event'] === null) {
62 EntityListenerBuilder::bindEntityListener($metadata, $listener['class']);
63 } else {
64 assert($listener['method'] !== null);
65 $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']);
66 }
67 }
68 }
69}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php
new file mode 100644
index 0000000..370f4fb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php
@@ -0,0 +1,25 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Tools\Console\EntityManagerProvider;
9use Symfony\Component\Console\Command\Command;
10use Symfony\Component\Console\Input\InputInterface;
11
12abstract class AbstractEntityManagerCommand extends Command
13{
14 public function __construct(private readonly EntityManagerProvider $entityManagerProvider)
15 {
16 parent::__construct();
17 }
18
19 final protected function getEntityManager(InputInterface $input): EntityManagerInterface
20 {
21 return $input->getOption('em') === null
22 ? $this->entityManagerProvider->getDefaultManager()
23 : $this->entityManagerProvider->getManager($input->getOption('em'));
24 }
25}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php
new file mode 100644
index 0000000..b4c6efa
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php
@@ -0,0 +1,119 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a collection cache region.
20 */
21class CollectionRegionCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:clear-cache:region:collection')
26 ->setDescription('Clear a second-level cache collection region')
27 ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.')
28 ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.')
29 ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.')
30 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
31 ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.')
32 ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
33 ->setHelp(<<<'EOT'
34The <info>%command.name%</info> command is meant to clear a second-level cache collection regions for an associated Entity Manager.
35It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider.
36
37The execution type differ on how you execute the command.
38If you want to invalidate all entries for an collection region this command would do the work:
39
40<info>%command.name% 'Entities\MyEntity' 'collectionName'</info>
41
42To invalidate a specific entry you should use :
43
44<info>%command.name% 'Entities\MyEntity' 'collectionName' 1</info>
45
46If you want to invalidate all entries for the all collection regions:
47
48<info>%command.name% --all</info>
49
50Alternatively, if you want to flush the configured cache provider for an collection region use this command:
51
52<info>%command.name% 'Entities\MyEntity' 'collectionName' --flush</info>
53
54Finally, be aware that if <info>--flush</info> option is passed,
55not all cache providers are able to flush entries, because of a limitation of its execution nature.
56EOT);
57 }
58
59 protected function execute(InputInterface $input, OutputInterface $output): int
60 {
61 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
62
63 $em = $this->getEntityManager($input);
64 $ownerClass = $input->getArgument('owner-class');
65 $assoc = $input->getArgument('association');
66 $ownerId = $input->getArgument('owner-id');
67 $cache = $em->getCache();
68
69 if (! $cache instanceof Cache) {
70 throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
71 }
72
73 if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) {
74 throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"');
75 }
76
77 if ($input->getOption('flush')) {
78 $cache->getCollectionCacheRegion($ownerClass, $assoc)
79 ->evictAll();
80
81 $ui->comment(
82 sprintf(
83 'Flushing cache provider configured for <info>"%s#%s"</info>',
84 $ownerClass,
85 $assoc,
86 ),
87 );
88
89 return 0;
90 }
91
92 if ($input->getOption('all')) {
93 $ui->comment('Clearing <info>all</info> second-level cache collection regions');
94
95 $cache->evictEntityRegions();
96
97 return 0;
98 }
99
100 if ($ownerId) {
101 $ui->comment(
102 sprintf(
103 'Clearing second-level cache entry for collection <info>"%s#%s"</info> owner entity identified by <info>"%s"</info>',
104 $ownerClass,
105 $assoc,
106 $ownerId,
107 ),
108 );
109 $cache->evictCollection($ownerClass, $assoc, $ownerId);
110
111 return 0;
112 }
113
114 $ui->comment(sprintf('Clearing second-level cache for collection <info>"%s#%s"</info>', $ownerClass, $assoc));
115 $cache->evictCollectionRegion($ownerClass, $assoc);
116
117 return 0;
118 }
119}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php
new file mode 100644
index 0000000..c5f2d65
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php
@@ -0,0 +1,110 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a entity cache region.
20 */
21class EntityRegionCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:clear-cache:region:entity')
26 ->setDescription('Clear a second-level cache entity region')
27 ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.')
28 ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.')
29 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
30 ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.')
31 ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
32 ->setHelp(<<<'EOT'
33The <info>%command.name%</info> command is meant to clear a second-level cache entity region for an associated Entity Manager.
34It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider.
35
36The execution type differ on how you execute the command.
37If you want to invalidate all entries for an entity region this command would do the work:
38
39<info>%command.name% 'Entities\MyEntity'</info>
40
41To invalidate a specific entry you should use :
42
43<info>%command.name% 'Entities\MyEntity' 1</info>
44
45If you want to invalidate all entries for the all entity regions:
46
47<info>%command.name% --all</info>
48
49Alternatively, if you want to flush the configured cache provider for an entity region use this command:
50
51<info>%command.name% 'Entities\MyEntity' --flush</info>
52
53Finally, be aware that if <info>--flush</info> option is passed,
54not all cache providers are able to flush entries, because of a limitation of its execution nature.
55EOT);
56 }
57
58 protected function execute(InputInterface $input, OutputInterface $output): int
59 {
60 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
61
62 $em = $this->getEntityManager($input);
63 $entityClass = $input->getArgument('entity-class');
64 $entityId = $input->getArgument('entity-id');
65 $cache = $em->getCache();
66
67 if (! $cache instanceof Cache) {
68 throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
69 }
70
71 if (! $entityClass && ! $input->getOption('all')) {
72 throw new InvalidArgumentException('Invalid argument "--entity-class"');
73 }
74
75 if ($input->getOption('flush')) {
76 $cache->getEntityCacheRegion($entityClass)
77 ->evictAll();
78
79 $ui->comment(sprintf('Flushing cache provider configured for entity named <info>"%s"</info>', $entityClass));
80
81 return 0;
82 }
83
84 if ($input->getOption('all')) {
85 $ui->comment('Clearing <info>all</info> second-level cache entity regions');
86
87 $cache->evictEntityRegions();
88
89 return 0;
90 }
91
92 if ($entityId) {
93 $ui->comment(
94 sprintf(
95 'Clearing second-level cache entry for entity <info>"%s"</info> identified by <info>"%s"</info>',
96 $entityClass,
97 $entityId,
98 ),
99 );
100 $cache->evictEntity($entityClass, $entityId);
101
102 return 0;
103 }
104
105 $ui->comment(sprintf('Clearing second-level cache for entity <info>"%s"</info>', $entityClass));
106 $cache->evictEntityRegion($entityClass);
107
108 return 0;
109 }
110}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php
new file mode 100644
index 0000000..147795b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php
@@ -0,0 +1,52 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use Symfony\Component\Console\Style\SymfonyStyle;
13
14/**
15 * Command to clear the metadata cache of the various cache drivers.
16 *
17 * @link www.doctrine-project.org
18 */
19class MetadataCommand extends AbstractEntityManagerCommand
20{
21 protected function configure(): void
22 {
23 $this->setName('orm:clear-cache:metadata')
24 ->setDescription('Clear all metadata cache of the various cache drivers')
25 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
26 ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.')
27 ->setHelp(<<<'EOT'
28The <info>%command.name%</info> command is meant to clear the metadata cache of associated Entity Manager.
29EOT);
30 }
31
32 protected function execute(InputInterface $input, OutputInterface $output): int
33 {
34 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
35
36 $em = $this->getEntityManager($input);
37 $cacheDriver = $em->getConfiguration()->getMetadataCache();
38
39 if (! $cacheDriver) {
40 throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.');
41 }
42
43 $ui->comment('Clearing <info>all</info> Metadata cache entries');
44
45 $result = $cacheDriver->clear();
46 $message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
47
48 $ui->success($message);
49
50 return 0;
51 }
52}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php
new file mode 100644
index 0000000..83edd7a
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php
@@ -0,0 +1,54 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use LogicException;
10use Symfony\Component\Cache\Adapter\ApcuAdapter;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16/**
17 * Command to clear the query cache of the various cache drivers.
18 *
19 * @link www.doctrine-project.org
20 */
21class QueryCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:clear-cache:query')
26 ->setDescription('Clear all query cache of the various cache drivers')
27 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
28 ->setHelp('The <info>%command.name%</info> command is meant to clear the query cache of associated Entity Manager.');
29 }
30
31 protected function execute(InputInterface $input, OutputInterface $output): int
32 {
33 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
34
35 $em = $this->getEntityManager($input);
36 $cache = $em->getConfiguration()->getQueryCache();
37
38 if (! $cache) {
39 throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.');
40 }
41
42 if ($cache instanceof ApcuAdapter) {
43 throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
44 }
45
46 $ui->comment('Clearing <info>all</info> Query cache entries');
47
48 $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
49
50 $ui->success($message);
51
52 return 0;
53 }
54}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php
new file mode 100644
index 0000000..e80fb90
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php
@@ -0,0 +1,101 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Cache;
8use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
9use InvalidArgumentException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function sprintf;
17
18/**
19 * Command to clear a query cache region.
20 */
21class QueryRegionCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:clear-cache:region:query')
26 ->setDescription('Clear a second-level cache query region')
27 ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.')
28 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
29 ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.')
30 ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.')
31 ->setHelp(<<<'EOT'
32The <info>%command.name%</info> command is meant to clear a second-level cache query region for an associated Entity Manager.
33It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider.
34
35The execution type differ on how you execute the command.
36If you want to invalidate all entries for the default query region this command would do the work:
37
38<info>%command.name%</info>
39
40To invalidate entries for a specific query region you should use :
41
42<info>%command.name% my_region_name</info>
43
44If you want to invalidate all entries for the all query region:
45
46<info>%command.name% --all</info>
47
48Alternatively, if you want to flush the configured cache provider use this command:
49
50<info>%command.name% my_region_name --flush</info>
51
52Finally, be aware that if <info>--flush</info> option is passed,
53not all cache providers are able to flush entries, because of a limitation of its execution nature.
54EOT);
55 }
56
57 protected function execute(InputInterface $input, OutputInterface $output): int
58 {
59 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
60
61 $em = $this->getEntityManager($input);
62 $name = $input->getArgument('region-name');
63 $cache = $em->getCache();
64
65 if ($name === null) {
66 $name = Cache::DEFAULT_QUERY_REGION_NAME;
67 }
68
69 if (! $cache instanceof Cache) {
70 throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.');
71 }
72
73 if ($input->getOption('flush')) {
74 $cache->getQueryCache($name)
75 ->getRegion()
76 ->evictAll();
77
78 $ui->comment(
79 sprintf(
80 'Flushing cache provider configured for second-level cache query region named <info>"%s"</info>',
81 $name,
82 ),
83 );
84
85 return 0;
86 }
87
88 if ($input->getOption('all')) {
89 $ui->comment('Clearing <info>all</info> second-level cache query regions');
90
91 $cache->evictQueryRegions();
92
93 return 0;
94 }
95
96 $ui->comment(sprintf('Clearing second-level cache query region named <info>"%s"</info>', $name));
97 $cache->evictQueryRegion($name);
98
99 return 0;
100 }
101}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php
new file mode 100644
index 0000000..4f84e0b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php
@@ -0,0 +1,65 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\ClearCache;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use Symfony\Component\Console\Style\SymfonyStyle;
13
14/**
15 * Command to clear the result cache of the various cache drivers.
16 *
17 * @link www.doctrine-project.org
18 */
19class ResultCommand extends AbstractEntityManagerCommand
20{
21 protected function configure(): void
22 {
23 $this->setName('orm:clear-cache:result')
24 ->setDescription('Clear all result cache of the various cache drivers')
25 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
26 ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.')
27 ->setHelp(<<<'EOT'
28The <info>%command.name%</info> command is meant to clear the result cache of associated Entity Manager.
29It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider
30instance completely.
31
32The execution type differ on how you execute the command.
33If you want to invalidate the entries (and not delete from cache instance), this command would do the work:
34
35<info>%command.name%</info>
36
37Alternatively, if you want to flush the cache provider using this command:
38
39<info>%command.name% --flush</info>
40
41Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries,
42because of a limitation of its execution nature.
43EOT);
44 }
45
46 protected function execute(InputInterface $input, OutputInterface $output): int
47 {
48 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
49
50 $em = $this->getEntityManager($input);
51 $cache = $em->getConfiguration()->getResultCache();
52
53 if (! $cache) {
54 throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.');
55 }
56
57 $ui->comment('Clearing <info>all</info> Result cache entries');
58
59 $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';
60
61 $ui->success($message);
62
63 return 0;
64 }
65}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php
new file mode 100644
index 0000000..5a407de
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php
@@ -0,0 +1,96 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\Console\MetadataFilter;
8use InvalidArgumentException;
9use Symfony\Component\Console\Input\InputArgument;
10use Symfony\Component\Console\Input\InputInterface;
11use Symfony\Component\Console\Input\InputOption;
12use Symfony\Component\Console\Output\OutputInterface;
13use Symfony\Component\Console\Style\SymfonyStyle;
14
15use function file_exists;
16use function is_dir;
17use function is_writable;
18use function mkdir;
19use function realpath;
20use function sprintf;
21
22/**
23 * Command to (re)generate the proxy classes used by doctrine.
24 *
25 * @link www.doctrine-project.org
26 */
27class GenerateProxiesCommand extends AbstractEntityManagerCommand
28{
29 protected function configure(): void
30 {
31 $this->setName('orm:generate-proxies')
32 ->setAliases(['orm:generate:proxies'])
33 ->setDescription('Generates proxy classes for entity classes')
34 ->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.')
35 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
36 ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.')
37 ->setHelp('Generates proxy classes for entity classes.');
38 }
39
40 protected function execute(InputInterface $input, OutputInterface $output): int
41 {
42 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
43
44 $em = $this->getEntityManager($input);
45
46 $metadatas = $em->getMetadataFactory()->getAllMetadata();
47 $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter'));
48
49 // Process destination directory
50 $destPath = $input->getArgument('dest-path');
51 if ($destPath === null) {
52 $destPath = $em->getConfiguration()->getProxyDir();
53
54 if ($destPath === null) {
55 throw new InvalidArgumentException('Proxy directory cannot be null');
56 }
57 }
58
59 if (! is_dir($destPath)) {
60 mkdir($destPath, 0775, true);
61 }
62
63 $destPath = realpath($destPath);
64
65 if (! file_exists($destPath)) {
66 throw new InvalidArgumentException(
67 sprintf("Proxies destination directory '<info>%s</info>' does not exist.", $em->getConfiguration()->getProxyDir()),
68 );
69 }
70
71 if (! is_writable($destPath)) {
72 throw new InvalidArgumentException(
73 sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath),
74 );
75 }
76
77 if (empty($metadatas)) {
78 $ui->success('No Metadata Classes to process.');
79
80 return 0;
81 }
82
83 foreach ($metadatas as $metadata) {
84 $ui->text(sprintf('Processing entity "<info>%s</info>"', $metadata->name));
85 }
86
87 // Generating Proxies
88 $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath);
89
90 // Outputting information message
91 $ui->newLine();
92 $ui->text(sprintf('Proxy classes generated to "<info>%s</info>"', $destPath));
93
94 return 0;
95 }
96}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php
new file mode 100644
index 0000000..deebb58
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php
@@ -0,0 +1,80 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Mapping\MappingException;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use function sprintf;
15
16/**
17 * Show information about mapped entities.
18 *
19 * @link www.doctrine-project.org
20 */
21class InfoCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:info')
26 ->setDescription('Show basic information about all mapped entities')
27 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
28 ->setHelp(<<<'EOT'
29The <info>%command.name%</info> shows basic information about which
30entities exist and possibly if their mapping information contains errors or
31not.
32EOT);
33 }
34
35 protected function execute(InputInterface $input, OutputInterface $output): int
36 {
37 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
38
39 $entityManager = $this->getEntityManager($input);
40
41 $entityClassNames = $entityManager->getConfiguration()
42 ->getMetadataDriverImpl()
43 ->getAllClassNames();
44
45 if (! $entityClassNames) {
46 $ui->caution(
47 [
48 'You do not have any mapped Doctrine ORM entities according to the current configuration.',
49 'If you have entities or mapping files you should check your mapping configuration for errors.',
50 ],
51 );
52
53 return 1;
54 }
55
56 $ui->text(sprintf('Found <info>%d</info> mapped entities:', count($entityClassNames)));
57 $ui->newLine();
58
59 $failure = false;
60
61 foreach ($entityClassNames as $entityClassName) {
62 try {
63 $entityManager->getClassMetadata($entityClassName);
64 $ui->text(sprintf('<info>[OK]</info> %s', $entityClassName));
65 } catch (MappingException $e) {
66 $ui->text(
67 [
68 sprintf('<error>[FAIL]</error> %s', $entityClassName),
69 sprintf('<comment>%s</comment>', $e->getMessage()),
70 '',
71 ],
72 );
73
74 $failure = true;
75 }
76 }
77
78 return $failure ? 1 : 0;
79 }
80}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php
new file mode 100644
index 0000000..41a177d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php
@@ -0,0 +1,279 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Mapping\AssociationMapping;
9use Doctrine\ORM\Mapping\ClassMetadata;
10use Doctrine\ORM\Mapping\FieldMapping;
11use Doctrine\Persistence\Mapping\MappingException;
12use InvalidArgumentException;
13use Symfony\Component\Console\Input\InputArgument;
14use Symfony\Component\Console\Input\InputInterface;
15use Symfony\Component\Console\Input\InputOption;
16use Symfony\Component\Console\Output\OutputInterface;
17use Symfony\Component\Console\Style\SymfonyStyle;
18
19use function array_filter;
20use function array_map;
21use function array_merge;
22use function count;
23use function current;
24use function get_debug_type;
25use function implode;
26use function is_array;
27use function is_bool;
28use function is_object;
29use function is_scalar;
30use function json_encode;
31use function preg_match;
32use function preg_quote;
33use function print_r;
34use function sprintf;
35
36use const JSON_PRETTY_PRINT;
37use const JSON_THROW_ON_ERROR;
38use const JSON_UNESCAPED_SLASHES;
39use const JSON_UNESCAPED_UNICODE;
40
41/**
42 * Show information about mapped entities.
43 *
44 * @link www.doctrine-project.org
45 */
46final class MappingDescribeCommand extends AbstractEntityManagerCommand
47{
48 protected function configure(): void
49 {
50 $this->setName('orm:mapping:describe')
51 ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
52 ->setDescription('Display information about mapped objects')
53 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
54 ->setHelp(<<<'EOT'
55The %command.full_name% command describes the metadata for the given full or partial entity class name.
56
57 <info>%command.full_name%</info> My\Namespace\Entity\MyEntity
58
59Or:
60
61 <info>%command.full_name%</info> MyEntity
62EOT);
63 }
64
65 protected function execute(InputInterface $input, OutputInterface $output): int
66 {
67 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
68
69 $entityManager = $this->getEntityManager($input);
70
71 $this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
72
73 return 0;
74 }
75
76 /**
77 * Display all the mapping information for a single Entity.
78 *
79 * @param string $entityName Full or partial entity class name
80 */
81 private function displayEntity(
82 string $entityName,
83 EntityManagerInterface $entityManager,
84 SymfonyStyle $ui,
85 ): void {
86 $metadata = $this->getClassMetadata($entityName, $entityManager);
87
88 $ui->table(
89 ['Field', 'Value'],
90 array_merge(
91 [
92 $this->formatField('Name', $metadata->name),
93 $this->formatField('Root entity name', $metadata->rootEntityName),
94 $this->formatField('Custom generator definition', $metadata->customGeneratorDefinition),
95 $this->formatField('Custom repository class', $metadata->customRepositoryClassName),
96 $this->formatField('Mapped super class?', $metadata->isMappedSuperclass),
97 $this->formatField('Embedded class?', $metadata->isEmbeddedClass),
98 $this->formatField('Parent classes', $metadata->parentClasses),
99 $this->formatField('Sub classes', $metadata->subClasses),
100 $this->formatField('Embedded classes', $metadata->subClasses),
101 $this->formatField('Identifier', $metadata->identifier),
102 $this->formatField('Inheritance type', $metadata->inheritanceType),
103 $this->formatField('Discriminator column', $metadata->discriminatorColumn),
104 $this->formatField('Discriminator value', $metadata->discriminatorValue),
105 $this->formatField('Discriminator map', $metadata->discriminatorMap),
106 $this->formatField('Generator type', $metadata->generatorType),
107 $this->formatField('Table', $metadata->table),
108 $this->formatField('Composite identifier?', $metadata->isIdentifierComposite),
109 $this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier),
110 $this->formatField('Enum identifier?', $metadata->containsEnumIdentifier),
111 $this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition),
112 $this->formatField('Change tracking policy', $metadata->changeTrackingPolicy),
113 $this->formatField('Versioned?', $metadata->isVersioned),
114 $this->formatField('Version field', $metadata->versionField),
115 $this->formatField('Read only?', $metadata->isReadOnly),
116
117 $this->formatEntityListeners($metadata->entityListeners),
118 ],
119 [$this->formatField('Association mappings:', '')],
120 $this->formatMappings($metadata->associationMappings),
121 [$this->formatField('Field mappings:', '')],
122 $this->formatMappings($metadata->fieldMappings),
123 ),
124 );
125 }
126
127 /**
128 * Return all mapped entity class names
129 *
130 * @return string[]
131 * @psalm-return class-string[]
132 */
133 private function getMappedEntities(EntityManagerInterface $entityManager): array
134 {
135 $entityClassNames = $entityManager->getConfiguration()
136 ->getMetadataDriverImpl()
137 ->getAllClassNames();
138
139 if (! $entityClassNames) {
140 throw new InvalidArgumentException(
141 'You do not have any mapped Doctrine ORM entities according to the current configuration. ' .
142 'If you have entities or mapping files you should check your mapping configuration for errors.',
143 );
144 }
145
146 return $entityClassNames;
147 }
148
149 /**
150 * Return the class metadata for the given entity
151 * name
152 *
153 * @param string $entityName Full or partial entity name
154 */
155 private function getClassMetadata(
156 string $entityName,
157 EntityManagerInterface $entityManager,
158 ): ClassMetadata {
159 try {
160 return $entityManager->getClassMetadata($entityName);
161 } catch (MappingException) {
162 }
163
164 $matches = array_filter(
165 $this->getMappedEntities($entityManager),
166 static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity)
167 );
168
169 if (! $matches) {
170 throw new InvalidArgumentException(sprintf(
171 'Could not find any mapped Entity classes matching "%s"',
172 $entityName,
173 ));
174 }
175
176 if (count($matches) > 1) {
177 throw new InvalidArgumentException(sprintf(
178 'Entity name "%s" is ambiguous, possible matches: "%s"',
179 $entityName,
180 implode(', ', $matches),
181 ));
182 }
183
184 return $entityManager->getClassMetadata(current($matches));
185 }
186
187 /**
188 * Format the given value for console output
189 */
190 private function formatValue(mixed $value): string
191 {
192 if ($value === '') {
193 return '';
194 }
195
196 if ($value === null) {
197 return '<comment>Null</comment>';
198 }
199
200 if (is_bool($value)) {
201 return '<comment>' . ($value ? 'True' : 'False') . '</comment>';
202 }
203
204 if (empty($value)) {
205 return '<comment>Empty</comment>';
206 }
207
208 if (is_array($value)) {
209 return json_encode(
210 $value,
211 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
212 );
213 }
214
215 if (is_object($value)) {
216 return sprintf('<%s>', get_debug_type($value));
217 }
218
219 if (is_scalar($value)) {
220 return (string) $value;
221 }
222
223 throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
224 }
225
226 /**
227 * Add the given label and value to the two column table output
228 *
229 * @param string $label Label for the value
230 * @param mixed $value A Value to show
231 *
232 * @return string[]
233 * @psalm-return array{0: string, 1: string}
234 */
235 private function formatField(string $label, mixed $value): array
236 {
237 if ($value === null) {
238 $value = '<comment>None</comment>';
239 }
240
241 return [sprintf('<info>%s</info>', $label), $this->formatValue($value)];
242 }
243
244 /**
245 * Format the association mappings
246 *
247 * @psalm-param array<string, FieldMapping|AssociationMapping> $propertyMappings
248 *
249 * @return string[][]
250 * @psalm-return list<array{0: string, 1: string}>
251 */
252 private function formatMappings(array $propertyMappings): array
253 {
254 $output = [];
255
256 foreach ($propertyMappings as $propertyName => $mapping) {
257 $output[] = $this->formatField(sprintf(' %s', $propertyName), '');
258
259 foreach ((array) $mapping as $field => $value) {
260 $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value));
261 }
262 }
263
264 return $output;
265 }
266
267 /**
268 * Format the entity listeners
269 *
270 * @psalm-param list<object> $entityListeners
271 *
272 * @return string[]
273 * @psalm-return array{0: string, 1: string}
274 */
275 private function formatEntityListeners(array $entityListeners): array
276 {
277 return $this->formatField('Entity listeners', array_map('get_class', $entityListeners));
278 }
279}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php
new file mode 100644
index 0000000..252151e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php
@@ -0,0 +1,118 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\Debug;
8use LogicException;
9use RuntimeException;
10use Symfony\Component\Console\Input\InputArgument;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Symfony\Component\Console\Style\SymfonyStyle;
15
16use function constant;
17use function defined;
18use function is_numeric;
19use function sprintf;
20use function str_replace;
21use function strtoupper;
22
23/**
24 * Command to execute DQL queries in a given EntityManager.
25 *
26 * @link www.doctrine-project.org
27 */
28class RunDqlCommand extends AbstractEntityManagerCommand
29{
30 protected function configure(): void
31 {
32 $this->setName('orm:run-dql')
33 ->setDescription('Executes arbitrary DQL directly from the command line')
34 ->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.')
35 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
36 ->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object')
37 ->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.')
38 ->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.')
39 ->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7)
40 ->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query')
41 ->setHelp(<<<'EOT'
42 The <info>%command.name%</info> command executes the given DQL query and
43 outputs the results:
44
45 <info>php %command.full_name% "SELECT u FROM App\Entity\User u"</info>
46
47 You can also optionally specify some additional options like what type of
48 hydration to use when executing the query:
49
50 <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --hydrate=array</info>
51
52 Additionally you can specify the first result and maximum amount of results to
53 show:
54
55 <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --first-result=0 --max-result=30</info>
56 EOT);
57 }
58
59 protected function execute(InputInterface $input, OutputInterface $output): int
60 {
61 $ui = new SymfonyStyle($input, $output);
62
63 $em = $this->getEntityManager($input);
64
65 $dql = $input->getArgument('dql');
66 if ($dql === null) {
67 throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly.");
68 }
69
70 $depth = $input->getOption('depth');
71
72 if (! is_numeric($depth)) {
73 throw new LogicException("Option 'depth' must contain an integer value");
74 }
75
76 $hydrationModeName = (string) $input->getOption('hydrate');
77 $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName));
78
79 if (! defined($hydrationMode)) {
80 throw new RuntimeException(sprintf(
81 "Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.",
82 $hydrationModeName,
83 ));
84 }
85
86 $query = $em->createQuery($dql);
87
88 $firstResult = $input->getOption('first-result');
89 if ($firstResult !== null) {
90 if (! is_numeric($firstResult)) {
91 throw new LogicException("Option 'first-result' must contain an integer value");
92 }
93
94 $query->setFirstResult((int) $firstResult);
95 }
96
97 $maxResult = $input->getOption('max-result');
98 if ($maxResult !== null) {
99 if (! is_numeric($maxResult)) {
100 throw new LogicException("Option 'max-result' must contain an integer value");
101 }
102
103 $query->setMaxResults((int) $maxResult);
104 }
105
106 if ($input->getOption('show-sql')) {
107 $ui->text($query->getSQL());
108
109 return 0;
110 }
111
112 $resultSet = $query->execute([], constant($hydrationMode));
113
114 $ui->text(Debug::dump($resultSet, (int) $input->getOption('depth')));
115
116 return 0;
117 }
118}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php
new file mode 100644
index 0000000..b1e4460
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php
@@ -0,0 +1,39 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
8use Doctrine\ORM\Tools\SchemaTool;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13/**
14 * Base class for CreateCommand, DropCommand and UpdateCommand.
15 *
16 * @link www.doctrine-project.org
17 */
18abstract class AbstractCommand extends AbstractEntityManagerCommand
19{
20 /** @param mixed[] $metadatas */
21 abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int;
22
23 protected function execute(InputInterface $input, OutputInterface $output): int
24 {
25 $ui = new SymfonyStyle($input, $output);
26
27 $em = $this->getEntityManager($input);
28
29 $metadatas = $em->getMetadataFactory()->getAllMetadata();
30
31 if (empty($metadatas)) {
32 $ui->getErrorStyle()->success('No Metadata Classes to process.');
33
34 return 0;
35 }
36
37 return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui);
38 }
39}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php
new file mode 100644
index 0000000..69e20c6
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php
@@ -0,0 +1,75 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\SchemaTool;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function sprintf;
14
15/**
16 * Command to create the database schema for a set of classes based on their mappings.
17 *
18 * @link www.doctrine-project.org
19 */
20class CreateCommand extends AbstractCommand
21{
22 protected function configure(): void
23 {
24 $this->setName('orm:schema-tool:create')
25 ->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output')
26 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
27 ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.')
28 ->setHelp(<<<'EOT'
29Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.
30
31<comment>Hint:</comment> If you have a database with tables that should not be managed
32by the ORM, you can use a DBAL functionality to filter the tables and sequences down
33on a global level:
34
35 $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
36 if ($assetName instanceof AbstractAsset) {
37 $assetName = $assetName->getName();
38 }
39
40 return !str_starts_with($assetName, 'audit_');
41 });
42EOT);
43 }
44
45 /**
46 * {@inheritDoc}
47 */
48 protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int
49 {
50 $dumpSql = $input->getOption('dump-sql') === true;
51
52 if ($dumpSql) {
53 $sqls = $schemaTool->getCreateSchemaSql($metadatas);
54
55 foreach ($sqls as $sql) {
56 $ui->writeln(sprintf('%s;', $sql));
57 }
58
59 return 0;
60 }
61
62 $notificationUi = $ui->getErrorStyle();
63
64 $notificationUi->caution('This operation should not be executed in a production environment!');
65
66 $notificationUi->text('Creating database schema...');
67 $notificationUi->newLine();
68
69 $schemaTool->createSchema($metadatas);
70
71 $notificationUi->success('Database schema created successfully!');
72
73 return 0;
74 }
75}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php
new file mode 100644
index 0000000..5c8253b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php
@@ -0,0 +1,116 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\ORM\Tools\SchemaTool;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use function sprintf;
15
16/**
17 * Command to drop the database schema for a set of classes based on their mappings.
18 *
19 * @link www.doctrine-project.org
20 */
21class DropCommand extends AbstractCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:schema-tool:drop')
26 ->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output')
27 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
28 ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.')
29 ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.")
30 ->addOption('full-database', null, InputOption::VALUE_NONE, 'Instead of using the Class Metadata to detect the database table schema, drop ALL assets that the database contains.')
31 ->setHelp(<<<'EOT'
32Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output.
33Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model.
34
35<comment>Hint:</comment> If you have a database with tables that should not be managed
36by the ORM, you can use a DBAL functionality to filter the tables and sequences down
37on a global level:
38
39 $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
40 if ($assetName instanceof AbstractAsset) {
41 $assetName = $assetName->getName();
42 }
43
44 return !str_starts_with($assetName, 'audit_');
45 });
46EOT);
47 }
48
49 /**
50 * {@inheritDoc}
51 */
52 protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int
53 {
54 $isFullDatabaseDrop = $input->getOption('full-database');
55 $dumpSql = $input->getOption('dump-sql') === true;
56 $force = $input->getOption('force') === true;
57
58 if ($dumpSql) {
59 if ($isFullDatabaseDrop) {
60 $sqls = $schemaTool->getDropDatabaseSQL();
61 } else {
62 $sqls = $schemaTool->getDropSchemaSQL($metadatas);
63 }
64
65 foreach ($sqls as $sql) {
66 $ui->writeln(sprintf('%s;', $sql));
67 }
68
69 return 0;
70 }
71
72 $notificationUi = $ui->getErrorStyle();
73
74 if ($force) {
75 $notificationUi->text('Dropping database schema...');
76 $notificationUi->newLine();
77
78 if ($isFullDatabaseDrop) {
79 $schemaTool->dropDatabase();
80 } else {
81 $schemaTool->dropSchema($metadatas);
82 }
83
84 $notificationUi->success('Database schema dropped successfully!');
85
86 return 0;
87 }
88
89 $notificationUi->caution('This operation should not be executed in a production environment!');
90
91 if ($isFullDatabaseDrop) {
92 $sqls = $schemaTool->getDropDatabaseSQL();
93 } else {
94 $sqls = $schemaTool->getDropSchemaSQL($metadatas);
95 }
96
97 if (empty($sqls)) {
98 $notificationUi->success('Nothing to drop. The database is empty!');
99
100 return 0;
101 }
102
103 $notificationUi->text(
104 [
105 sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)),
106 '',
107 'Please run the operation by passing one - or both - of the following options:',
108 '',
109 sprintf(' <info>%s --force</info> to execute the command', $this->getName()),
110 sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()),
111 ],
112 );
113
114 return 1;
115 }
116}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php
new file mode 100644
index 0000000..f35fc38
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php
@@ -0,0 +1,147 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command\SchemaTool;
6
7use Doctrine\Deprecations\Deprecation;
8use Doctrine\ORM\Tools\SchemaTool;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use Symfony\Component\Console\Style\SymfonyStyle;
13
14use function count;
15use function sprintf;
16
17/**
18 * Command to generate the SQL needed to update the database schema to match
19 * the current mapping information.
20 *
21 * @link www.doctrine-project.org
22 */
23class UpdateCommand extends AbstractCommand
24{
25 protected string $name = 'orm:schema-tool:update';
26
27 protected function configure(): void
28 {
29 $this->setName($this->name)
30 ->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata')
31 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
32 ->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0')
33 ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).')
34 ->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.')
35 ->setHelp(<<<'EOT'
36The <info>%command.name%</info> command generates the SQL needed to
37synchronize the database schema with the current mapping metadata of the
38default entity manager.
39
40For example, if you add metadata for a new column to an entity, this command
41would generate and output the SQL needed to add the new column to the database:
42
43<info>%command.name% --dump-sql</info>
44
45Alternatively, you can execute the generated queries:
46
47<info>%command.name% --force</info>
48
49If both options are specified, the queries are output and then executed:
50
51<info>%command.name% --dump-sql --force</info>
52
53Finally, be aware that this task will drop all database assets (e.g. tables,
54etc) that are *not* described by the current metadata. In other words, without
55this option, this task leaves untouched any "extra" tables that exist in the
56database, but which aren't described by any metadata.
57
58<comment>Hint:</comment> If you have a database with tables that should not be managed
59by the ORM, you can use a DBAL functionality to filter the tables and sequences down
60on a global level:
61
62 $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
63 if ($assetName instanceof AbstractAsset) {
64 $assetName = $assetName->getName();
65 }
66
67 return !str_starts_with($assetName, 'audit_');
68 });
69EOT);
70 }
71
72 /**
73 * {@inheritDoc}
74 */
75 protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int
76 {
77 $notificationUi = $ui->getErrorStyle();
78
79 if ($input->getOption('complete') === true) {
80 Deprecation::trigger(
81 'doctrine/orm',
82 'https://github.com/doctrine/orm/pull/11354',
83 'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.',
84 );
85 $notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.');
86 }
87
88 $sqls = $schemaTool->getUpdateSchemaSql($metadatas);
89
90 if (empty($sqls)) {
91 $notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.');
92
93 return 0;
94 }
95
96 $dumpSql = $input->getOption('dump-sql') === true;
97 $force = $input->getOption('force') === true;
98
99 if ($dumpSql) {
100 foreach ($sqls as $sql) {
101 $ui->writeln(sprintf('%s;', $sql));
102 }
103 }
104
105 if ($force) {
106 if ($dumpSql) {
107 $notificationUi->newLine();
108 }
109
110 $notificationUi->text('Updating database schema...');
111 $notificationUi->newLine();
112
113 $schemaTool->updateSchema($metadatas);
114
115 $pluralization = count($sqls) === 1 ? 'query was' : 'queries were';
116
117 $notificationUi->text(sprintf(' <info>%s</info> %s executed', count($sqls), $pluralization));
118 $notificationUi->success('Database schema updated successfully!');
119 }
120
121 if ($dumpSql || $force) {
122 return 0;
123 }
124
125 $notificationUi->caution(
126 [
127 'This operation should not be executed in a production environment!',
128 '',
129 'Use the incremental update to detect changes during development and use',
130 'the SQL DDL provided to manually update your database in production.',
131 ],
132 );
133
134 $notificationUi->text(
135 [
136 sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)),
137 '',
138 'Please run the operation by passing one - or both - of the following options:',
139 '',
140 sprintf(' <info>%s --force</info> to execute the command', $this->getName()),
141 sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()),
142 ],
143 );
144
145 return 1;
146 }
147}
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php
new file mode 100644
index 0000000..cffb4ce
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php
@@ -0,0 +1,89 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\Command;
6
7use Doctrine\ORM\Tools\SchemaValidator;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Input\InputOption;
10use Symfony\Component\Console\Output\OutputInterface;
11use Symfony\Component\Console\Style\SymfonyStyle;
12
13use function count;
14use function sprintf;
15
16/**
17 * Command to validate that the current mapping is valid.
18 *
19 * @link www.doctrine-project.com
20 */
21class ValidateSchemaCommand extends AbstractEntityManagerCommand
22{
23 protected function configure(): void
24 {
25 $this->setName('orm:validate-schema')
26 ->setDescription('Validate the mapping files')
27 ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
28 ->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check')
29 ->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database')
30 ->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types')
31 ->setHelp('Validate that the mapping files are correct and in sync with the database.');
32 }
33
34 protected function execute(InputInterface $input, OutputInterface $output): int
35 {
36 $ui = (new SymfonyStyle($input, $output))->getErrorStyle();
37
38 $em = $this->getEntityManager($input);
39 $validator = new SchemaValidator($em, ! $input->getOption('skip-property-types'));
40 $exit = 0;
41
42 $ui->section('Mapping');
43
44 if ($input->getOption('skip-mapping')) {
45 $ui->text('<comment>[SKIPPED] The mapping was not checked.</comment>');
46 } else {
47 $errors = $validator->validateMapping();
48 if ($errors) {
49 foreach ($errors as $className => $errorMessages) {
50 $ui->text(
51 sprintf(
52 '<error>[FAIL]</error> The entity-class <comment>%s</comment> mapping is invalid:',
53 $className,
54 ),
55 );
56
57 $ui->listing($errorMessages);
58 $ui->newLine();
59 }
60
61 ++$exit;
62 } else {
63 $ui->success('The mapping files are correct.');
64 }
65 }
66
67 $ui->section('Database');
68
69 if ($input->getOption('skip-sync')) {
70 $ui->text('<comment>[SKIPPED] The database was not checked for synchronicity.</comment>');
71 } elseif (! $validator->schemaInSyncWithMetadata()) {
72 $ui->error('The database schema is not in sync with the current mapping file.');
73
74 if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
75 $sqls = $validator->getUpdateSchemaList();
76 $ui->comment(sprintf('<info>%d</info> schema diff(s) detected:', count($sqls)));
77 foreach ($sqls as $sql) {
78 $ui->text(sprintf(' %s;', $sql));
79 }
80 }
81
82 $exit += 2;
83 } else {
84 $ui->success('The database schema is in sync with the mapping files.');
85 }
86
87 return $exit;
88 }
89}
diff --git a/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php
new file mode 100644
index 0000000..0a00483
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use Composer\InstalledVersions;
8use Doctrine\DBAL\Tools\Console as DBALConsole;
9use Doctrine\ORM\Tools\Console\EntityManagerProvider\ConnectionFromManagerProvider;
10use OutOfBoundsException;
11use Symfony\Component\Console\Application;
12use Symfony\Component\Console\Command\Command as SymfonyCommand;
13
14use function assert;
15use function class_exists;
16
17/**
18 * Handles running the Console Tools inside Symfony Console context.
19 */
20final class ConsoleRunner
21{
22 /**
23 * Runs console with the given helper set.
24 *
25 * @param SymfonyCommand[] $commands
26 */
27 public static function run(EntityManagerProvider $entityManagerProvider, array $commands = []): void
28 {
29 $cli = self::createApplication($entityManagerProvider, $commands);
30 $cli->run();
31 }
32
33 /**
34 * Creates a console application with the given helperset and
35 * optional commands.
36 *
37 * @param SymfonyCommand[] $commands
38 *
39 * @throws OutOfBoundsException
40 */
41 public static function createApplication(
42 EntityManagerProvider $entityManagerProvider,
43 array $commands = [],
44 ): Application {
45 $version = InstalledVersions::getVersion('doctrine/orm');
46 assert($version !== null);
47
48 $cli = new Application('Doctrine Command Line Interface', $version);
49 $cli->setCatchExceptions(true);
50
51 self::addCommands($cli, $entityManagerProvider);
52 $cli->addCommands($commands);
53
54 return $cli;
55 }
56
57 public static function addCommands(Application $cli, EntityManagerProvider $entityManagerProvider): void
58 {
59 $connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider);
60
61 if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) {
62 $cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider));
63 }
64
65 $cli->addCommands(
66 [
67 // DBAL Commands
68 new DBALConsole\Command\RunSqlCommand($connectionProvider),
69
70 // ORM Commands
71 new Command\ClearCache\CollectionRegionCommand($entityManagerProvider),
72 new Command\ClearCache\EntityRegionCommand($entityManagerProvider),
73 new Command\ClearCache\MetadataCommand($entityManagerProvider),
74 new Command\ClearCache\QueryCommand($entityManagerProvider),
75 new Command\ClearCache\QueryRegionCommand($entityManagerProvider),
76 new Command\ClearCache\ResultCommand($entityManagerProvider),
77 new Command\SchemaTool\CreateCommand($entityManagerProvider),
78 new Command\SchemaTool\UpdateCommand($entityManagerProvider),
79 new Command\SchemaTool\DropCommand($entityManagerProvider),
80 new Command\GenerateProxiesCommand($entityManagerProvider),
81 new Command\RunDqlCommand($entityManagerProvider),
82 new Command\ValidateSchemaCommand($entityManagerProvider),
83 new Command\InfoCommand($entityManagerProvider),
84 new Command\MappingDescribeCommand($entityManagerProvider),
85 ],
86 );
87 }
88}
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php
new file mode 100644
index 0000000..866589b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php
@@ -0,0 +1,14 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use Doctrine\ORM\EntityManagerInterface;
8
9interface EntityManagerProvider
10{
11 public function getDefaultManager(): EntityManagerInterface;
12
13 public function getManager(string $name): EntityManagerInterface;
14}
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php
new file mode 100644
index 0000000..0776601
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Tools\Console\ConnectionProvider;
9use Doctrine\ORM\Tools\Console\EntityManagerProvider;
10
11final class ConnectionFromManagerProvider implements ConnectionProvider
12{
13 public function __construct(private readonly EntityManagerProvider $entityManagerProvider)
14 {
15 }
16
17 public function getDefaultConnection(): Connection
18 {
19 return $this->entityManagerProvider->getDefaultManager()->getConnection();
20 }
21
22 public function getConnection(string $name): Connection
23 {
24 return $this->entityManagerProvider->getManager($name)->getConnection();
25 }
26}
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php
new file mode 100644
index 0000000..ebe60c9
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php
@@ -0,0 +1,31 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Tools\Console\EntityManagerProvider;
9
10final class SingleManagerProvider implements EntityManagerProvider
11{
12 public function __construct(
13 private readonly EntityManagerInterface $entityManager,
14 private readonly string $defaultManagerName = 'default',
15 ) {
16 }
17
18 public function getDefaultManager(): EntityManagerInterface
19 {
20 return $this->entityManager;
21 }
22
23 public function getManager(string $name): EntityManagerInterface
24 {
25 if ($name !== $this->defaultManagerName) {
26 throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]);
27 }
28
29 return $this->entityManager;
30 }
31}
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php
new file mode 100644
index 0000000..583d909
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console\EntityManagerProvider;
6
7use OutOfBoundsException;
8
9use function implode;
10use function sprintf;
11
12final class UnknownManagerException extends OutOfBoundsException
13{
14 /** @psalm-param list<string> $knownManagers */
15 public static function unknownManager(string $unknownManager, array $knownManagers = []): self
16 {
17 return new self(sprintf(
18 'Requested unknown entity manager: %s, known managers: %s',
19 $unknownManager,
20 implode(', ', $knownManagers),
21 ));
22 }
23}
diff --git a/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php
new file mode 100644
index 0000000..05e248c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php
@@ -0,0 +1,92 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Console;
6
7use ArrayIterator;
8use Countable;
9use Doctrine\Persistence\Mapping\ClassMetadata;
10use FilterIterator;
11use RuntimeException;
12
13use function assert;
14use function count;
15use function iterator_to_array;
16use function preg_match;
17use function sprintf;
18
19/**
20 * Used by CLI Tools to restrict entity-based commands to given patterns.
21 *
22 * @link www.doctrine-project.com
23 */
24class MetadataFilter extends FilterIterator implements Countable
25{
26 /** @var mixed[] */
27 private array $filter = [];
28
29 /**
30 * Filter Metadatas by one or more filter options.
31 *
32 * @param ClassMetadata[] $metadatas
33 * @param string[]|string $filter
34 *
35 * @return ClassMetadata[]
36 */
37 public static function filter(array $metadatas, array|string $filter): array
38 {
39 $metadatas = new MetadataFilter(new ArrayIterator($metadatas), $filter);
40
41 return iterator_to_array($metadatas);
42 }
43
44 /** @param mixed[]|string $filter */
45 public function __construct(ArrayIterator $metadata, array|string $filter)
46 {
47 $this->filter = (array) $filter;
48
49 parent::__construct($metadata);
50 }
51
52 public function accept(): bool
53 {
54 if (count($this->filter) === 0) {
55 return true;
56 }
57
58 $it = $this->getInnerIterator();
59 $metadata = $it->current();
60
61 foreach ($this->filter as $filter) {
62 $pregResult = preg_match('/' . $filter . '/', $metadata->getName());
63
64 if ($pregResult === false) {
65 throw new RuntimeException(
66 sprintf("Error while evaluating regex '/%s/'.", $filter),
67 );
68 }
69
70 if ($pregResult) {
71 return true;
72 }
73 }
74
75 return false;
76 }
77
78 /** @return ArrayIterator<int, ClassMetadata> */
79 public function getInnerIterator(): ArrayIterator
80 {
81 $innerIterator = parent::getInnerIterator();
82
83 assert($innerIterator instanceof ArrayIterator);
84
85 return $innerIterator;
86 }
87
88 public function count(): int
89 {
90 return count($this->getInnerIterator());
91 }
92}
diff --git a/vendor/doctrine/orm/src/Tools/Debug.php b/vendor/doctrine/orm/src/Tools/Debug.php
new file mode 100644
index 0000000..8521e53
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Debug.php
@@ -0,0 +1,158 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use ArrayIterator;
8use ArrayObject;
9use DateTimeInterface;
10use Doctrine\Common\Collections\Collection;
11use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
12use Doctrine\Persistence\Proxy;
13use stdClass;
14
15use function array_keys;
16use function count;
17use function end;
18use function explode;
19use function extension_loaded;
20use function html_entity_decode;
21use function ini_get;
22use function ini_set;
23use function is_array;
24use function is_object;
25use function ob_end_clean;
26use function ob_get_contents;
27use function ob_start;
28use function strip_tags;
29use function var_dump;
30
31/**
32 * Static class containing most used debug methods.
33 *
34 * @internal
35 *
36 * @link www.doctrine-project.org
37 */
38final class Debug
39{
40 /**
41 * Private constructor (prevents instantiation).
42 */
43 private function __construct()
44 {
45 }
46
47 /**
48 * Prints a dump of the public, protected and private properties of $var.
49 *
50 * @link https://xdebug.org/
51 *
52 * @param mixed $var The variable to dump.
53 * @param int $maxDepth The maximum nesting level for object properties.
54 */
55 public static function dump(mixed $var, int $maxDepth = 2): string
56 {
57 $html = ini_get('html_errors');
58
59 if ($html !== '1') {
60 ini_set('html_errors', 'on');
61 }
62
63 if (extension_loaded('xdebug')) {
64 $previousDepth = ini_get('xdebug.var_display_max_depth');
65 ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
66 }
67
68 try {
69 $var = self::export($var, $maxDepth);
70
71 ob_start();
72 var_dump($var);
73
74 $dump = ob_get_contents();
75
76 ob_end_clean();
77
78 $dumpText = strip_tags(html_entity_decode($dump));
79 } finally {
80 ini_set('html_errors', $html);
81
82 if (isset($previousDepth)) {
83 ini_set('xdebug.var_display_max_depth', $previousDepth);
84 }
85 }
86
87 return $dumpText;
88 }
89
90 public static function export(mixed $var, int $maxDepth): mixed
91 {
92 if ($var instanceof Collection) {
93 $var = $var->toArray();
94 }
95
96 if (! $maxDepth) {
97 return is_object($var) ? $var::class
98 : (is_array($var) ? 'Array(' . count($var) . ')' : $var);
99 }
100
101 if (is_array($var)) {
102 $return = [];
103
104 foreach ($var as $k => $v) {
105 $return[$k] = self::export($v, $maxDepth - 1);
106 }
107
108 return $return;
109 }
110
111 if (! is_object($var)) {
112 return $var;
113 }
114
115 $return = new stdClass();
116 if ($var instanceof DateTimeInterface) {
117 $return->__CLASS__ = $var::class;
118 $return->date = $var->format('c');
119 $return->timezone = $var->getTimezone()->getName();
120
121 return $return;
122 }
123
124 $return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var);
125
126 if ($var instanceof Proxy) {
127 $return->__IS_PROXY__ = true;
128 $return->__PROXY_INITIALIZED__ = $var->__isInitialized();
129 }
130
131 if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
132 $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
133 }
134
135 return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
136 }
137
138 /**
139 * Fill the $return variable with class attributes
140 * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
141 */
142 private static function fillReturnWithClassAttributes(object $var, stdClass $return, int $maxDepth): stdClass
143 {
144 $clone = (array) $var;
145
146 foreach (array_keys($clone) as $key) {
147 $aux = explode("\0", (string) $key);
148 $name = end($aux);
149 if ($aux[0] === '') {
150 $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
151 }
152
153 $return->$name = self::export($clone[$key], $maxDepth - 1);
154 }
155
156 return $return;
157 }
158}
diff --git a/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php
new file mode 100644
index 0000000..71059f7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php
@@ -0,0 +1,144 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Event\OnFlushEventArgs;
9use Doctrine\ORM\PersistentCollection;
10use Doctrine\ORM\UnitOfWork;
11use ReflectionObject;
12
13use function count;
14use function fclose;
15use function fopen;
16use function fwrite;
17use function gettype;
18use function is_object;
19use function spl_object_id;
20
21/**
22 * Use this logger to dump the identity map during the onFlush event. This is useful for debugging
23 * weird UnitOfWork behavior with complex operations.
24 */
25class DebugUnitOfWorkListener
26{
27 /**
28 * Pass a stream and context information for the debugging session.
29 *
30 * The stream can be php://output to print to the screen.
31 */
32 public function __construct(
33 private readonly string $file = 'php://output',
34 private readonly string $context = '',
35 ) {
36 }
37
38 public function onFlush(OnFlushEventArgs $args): void
39 {
40 $this->dumpIdentityMap($args->getObjectManager());
41 }
42
43 /**
44 * Dumps the contents of the identity map into a stream.
45 */
46 public function dumpIdentityMap(EntityManagerInterface $em): void
47 {
48 $uow = $em->getUnitOfWork();
49 $identityMap = $uow->getIdentityMap();
50
51 $fh = fopen($this->file, 'xb+');
52 if (count($identityMap) === 0) {
53 fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n");
54
55 return;
56 }
57
58 fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n");
59 foreach ($identityMap as $className => $map) {
60 fwrite($fh, 'Class: ' . $className . "\n");
61
62 foreach ($map as $entity) {
63 fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n");
64 fwrite($fh, " Associations:\n");
65
66 $cm = $em->getClassMetadata($className);
67
68 foreach ($cm->associationMappings as $field => $assoc) {
69 fwrite($fh, ' ' . $field . ' ');
70 $value = $cm->getFieldValue($entity, $field);
71
72 if ($assoc->isToOne()) {
73 if ($value === null) {
74 fwrite($fh, " NULL\n");
75 } else {
76 if ($uow->isUninitializedObject($value)) {
77 fwrite($fh, '[PROXY] ');
78 }
79
80 fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n");
81 }
82 } else {
83 $initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized();
84 if ($value === null) {
85 fwrite($fh, " NULL\n");
86 } elseif ($initialized) {
87 fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n");
88
89 foreach ($value as $obj) {
90 fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n");
91 }
92 } else {
93 fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n");
94 foreach ($value->unwrap() as $obj) {
95 fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n");
96 }
97 }
98 }
99 }
100 }
101 }
102
103 fclose($fh);
104 }
105
106 private function getType(mixed $var): string
107 {
108 if (is_object($var)) {
109 $refl = new ReflectionObject($var);
110
111 return $refl->getShortName();
112 }
113
114 return gettype($var);
115 }
116
117 private function getIdString(object $entity, UnitOfWork $uow): string
118 {
119 if ($uow->isInIdentityMap($entity)) {
120 $ids = $uow->getEntityIdentifier($entity);
121 $idstring = '';
122
123 foreach ($ids as $k => $v) {
124 $idstring .= $k . '=' . $v;
125 }
126 } else {
127 $idstring = 'NEWOBJECT ';
128 }
129
130 $state = $uow->getEntityState($entity);
131
132 if ($state === UnitOfWork::STATE_NEW) {
133 $idstring .= ' [NEW]';
134 } elseif ($state === UnitOfWork::STATE_REMOVED) {
135 $idstring .= ' [REMOVED]';
136 } elseif ($state === UnitOfWork::STATE_MANAGED) {
137 $idstring .= ' [MANAGED]';
138 } elseif ($state === UnitOfWork::STATE_DETACHED) {
139 $idstring .= ' [DETACHED]';
140 }
141
142 return $idstring;
143 }
144}
diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php
new file mode 100644
index 0000000..3b0993e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Event;
6
7use Doctrine\Common\EventArgs;
8use Doctrine\DBAL\Schema\Schema;
9use Doctrine\ORM\EntityManagerInterface;
10
11/**
12 * Event Args used for the Events::postGenerateSchema event.
13 *
14 * @link www.doctrine-project.com
15 */
16class GenerateSchemaEventArgs extends EventArgs
17{
18 public function __construct(
19 private readonly EntityManagerInterface $em,
20 private readonly Schema $schema,
21 ) {
22 }
23
24 public function getEntityManager(): EntityManagerInterface
25 {
26 return $this->em;
27 }
28
29 public function getSchema(): Schema
30 {
31 return $this->schema;
32 }
33}
diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php
new file mode 100644
index 0000000..a09aaae
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Event;
6
7use Doctrine\Common\EventArgs;
8use Doctrine\DBAL\Schema\Schema;
9use Doctrine\DBAL\Schema\Table;
10use Doctrine\ORM\Mapping\ClassMetadata;
11
12/**
13 * Event Args used for the Events::postGenerateSchemaTable event.
14 *
15 * @link www.doctrine-project.com
16 */
17class GenerateSchemaTableEventArgs extends EventArgs
18{
19 public function __construct(
20 private readonly ClassMetadata $classMetadata,
21 private readonly Schema $schema,
22 private readonly Table $classTable,
23 ) {
24 }
25
26 public function getClassMetadata(): ClassMetadata
27 {
28 return $this->classMetadata;
29 }
30
31 public function getSchema(): Schema
32 {
33 return $this->schema;
34 }
35
36 public function getClassTable(): Table
37 {
38 return $this->classTable;
39 }
40}
diff --git a/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php
new file mode 100644
index 0000000..764721e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10use function sprintf;
11
12final class MissingColumnException extends LogicException implements ORMException
13{
14 public static function fromColumnSourceAndTarget(string $column, string $source, string $target): self
15 {
16 return new self(sprintf(
17 'Column name "%s" referenced for relation from %s towards %s does not exist.',
18 $column,
19 $source,
20 $target,
21 ));
22 }
23}
diff --git a/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php
new file mode 100644
index 0000000..af619fd
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Exception;
6
7use Doctrine\ORM\Exception\SchemaToolException;
8use LogicException;
9
10final class NotSupported extends LogicException implements SchemaToolException
11{
12 public static function create(): self
13 {
14 return new self('This behaviour is (currently) not supported by Doctrine 2');
15 }
16}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php
new file mode 100644
index 0000000..c7f31db
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php
@@ -0,0 +1,125 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\SQLServerPlatform;
9use Doctrine\ORM\Query;
10use Doctrine\ORM\Query\AST\SelectStatement;
11use Doctrine\ORM\Query\Parser;
12use Doctrine\ORM\Query\ParserResult;
13use Doctrine\ORM\Query\ResultSetMapping;
14use Doctrine\ORM\Query\SqlWalker;
15use RuntimeException;
16
17use function array_diff;
18use function array_keys;
19use function assert;
20use function count;
21use function implode;
22use function reset;
23use function sprintf;
24
25/**
26 * Wraps the query in order to accurately count the root objects.
27 *
28 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
29 * SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>))
30 *
31 * Works with composite keys but cannot deal with queries that have multiple
32 * root entities (e.g. `SELECT f, b from Foo, Bar`)
33 *
34 * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL)
35 * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery
36 * that will most likely be executed next can be read from the native SQL cache.
37 *
38 * @psalm-import-type QueryComponent from Parser
39 */
40class CountOutputWalker extends SqlWalker
41{
42 private readonly AbstractPlatform $platform;
43 private readonly ResultSetMapping $rsm;
44
45 /**
46 * {@inheritDoc}
47 */
48 public function __construct(Query $query, ParserResult $parserResult, array $queryComponents)
49 {
50 $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
51 $this->rsm = $parserResult->getResultSetMapping();
52
53 parent::__construct($query, $parserResult, $queryComponents);
54 }
55
56 public function walkSelectStatement(SelectStatement $selectStatement): string
57 {
58 if ($this->platform instanceof SQLServerPlatform) {
59 $selectStatement->orderByClause = null;
60 }
61
62 $sql = parent::walkSelectStatement($selectStatement);
63
64 if ($selectStatement->groupByClause) {
65 return sprintf(
66 'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table',
67 $sql,
68 );
69 }
70
71 // Find out the SQL alias of the identifier column of the root entity
72 // It may be possible to make this work with multiple root entities but that
73 // would probably require issuing multiple queries or doing a UNION SELECT
74 // so for now, It's not supported.
75
76 // Get the root entity and alias from the AST fromClause
77 $from = $selectStatement->fromClause->identificationVariableDeclarations;
78 if (count($from) > 1) {
79 throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
80 }
81
82 $fromRoot = reset($from);
83 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
84 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
85 $rootIdentifier = $rootClass->identifier;
86
87 // For every identifier, find out the SQL alias by combing through the ResultSetMapping
88 $sqlIdentifier = [];
89 foreach ($rootIdentifier as $property) {
90 if (isset($rootClass->fieldMappings[$property])) {
91 foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) {
92 if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
93 $sqlIdentifier[$property] = $alias;
94 }
95 }
96 }
97
98 if (isset($rootClass->associationMappings[$property])) {
99 $association = $rootClass->associationMappings[$property];
100 assert($association->isToOneOwningSide());
101 $joinColumn = $association->joinColumns[0]->name;
102
103 foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) {
104 if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
105 $sqlIdentifier[$property] = $alias;
106 }
107 }
108 }
109 }
110
111 if (count($rootIdentifier) !== count($sqlIdentifier)) {
112 throw new RuntimeException(sprintf(
113 'Not all identifier properties can be found in the ResultSetMapping: %s',
114 implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))),
115 ));
116 }
117
118 // Build the counter query
119 return sprintf(
120 'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table',
121 implode(', ', $sqlIdentifier),
122 $sql,
123 );
124 }
125}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php
new file mode 100644
index 0000000..d212943
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php
@@ -0,0 +1,68 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\AggregateExpression;
8use Doctrine\ORM\Query\AST\PathExpression;
9use Doctrine\ORM\Query\AST\SelectExpression;
10use Doctrine\ORM\Query\AST\SelectStatement;
11use Doctrine\ORM\Query\TreeWalkerAdapter;
12use RuntimeException;
13
14use function count;
15use function reset;
16
17/**
18 * Replaces the selectClause of the AST with a COUNT statement.
19 */
20class CountWalker extends TreeWalkerAdapter
21{
22 /**
23 * Distinct mode hint name.
24 */
25 public const HINT_DISTINCT = 'doctrine_paginator.distinct';
26
27 public function walkSelectStatement(SelectStatement $selectStatement): void
28 {
29 if ($selectStatement->havingClause) {
30 throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination');
31 }
32
33 // Get the root entity and alias from the AST fromClause
34 $from = $selectStatement->fromClause->identificationVariableDeclarations;
35
36 if (count($from) > 1) {
37 throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
38 }
39
40 $fromRoot = reset($from);
41 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
42 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
43 $identifierFieldName = $rootClass->getSingleIdentifierFieldName();
44
45 $pathType = PathExpression::TYPE_STATE_FIELD;
46 if (isset($rootClass->associationMappings[$identifierFieldName])) {
47 $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
48 }
49
50 $pathExpression = new PathExpression(
51 PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
52 $rootAlias,
53 $identifierFieldName,
54 );
55 $pathExpression->type = $pathType;
56
57 $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
58 $selectStatement->selectClause->selectExpressions = [
59 new SelectExpression(
60 new AggregateExpression('count', $pathExpression, $distinct),
61 null,
62 ),
63 ];
64
65 // ORDER BY is not needed, only increases query execution through unnecessary sorting.
66 $selectStatement->orderByClause = null;
67 }
68}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php
new file mode 100644
index 0000000..0e3da93
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php
@@ -0,0 +1,16 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination\Exception;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10final class RowNumberOverFunctionNotEnabled extends LogicException implements ORMException
11{
12 public static function create(): self
13 {
14 return new self('The RowNumberOverFunction is not intended for, nor is it enabled for use in DQL.');
15 }
16}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php
new file mode 100644
index 0000000..8bbc44c
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php
@@ -0,0 +1,544 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\DB2Platform;
9use Doctrine\DBAL\Platforms\OraclePlatform;
10use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
11use Doctrine\DBAL\Platforms\SQLServerPlatform;
12use Doctrine\ORM\EntityManagerInterface;
13use Doctrine\ORM\Mapping\QuoteStrategy;
14use Doctrine\ORM\OptimisticLockException;
15use Doctrine\ORM\Query;
16use Doctrine\ORM\Query\AST\OrderByClause;
17use Doctrine\ORM\Query\AST\PathExpression;
18use Doctrine\ORM\Query\AST\SelectExpression;
19use Doctrine\ORM\Query\AST\SelectStatement;
20use Doctrine\ORM\Query\AST\Subselect;
21use Doctrine\ORM\Query\Parser;
22use Doctrine\ORM\Query\ParserResult;
23use Doctrine\ORM\Query\QueryException;
24use Doctrine\ORM\Query\ResultSetMapping;
25use Doctrine\ORM\Query\SqlWalker;
26use RuntimeException;
27
28use function array_diff;
29use function array_keys;
30use function assert;
31use function count;
32use function implode;
33use function in_array;
34use function is_string;
35use function method_exists;
36use function preg_replace;
37use function reset;
38use function sprintf;
39use function strrpos;
40use function substr;
41
42/**
43 * Wraps the query in order to select root entity IDs for pagination.
44 *
45 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
46 * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
47 *
48 * Works with composite keys but cannot deal with queries that have multiple
49 * root entities (e.g. `SELECT f, b from Foo, Bar`)
50 *
51 * @psalm-import-type QueryComponent from Parser
52 */
53class LimitSubqueryOutputWalker extends SqlWalker
54{
55 private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
56
57 private readonly AbstractPlatform $platform;
58 private readonly ResultSetMapping $rsm;
59 private readonly int $firstResult;
60 private readonly int|null $maxResults;
61 private readonly EntityManagerInterface $em;
62 private readonly QuoteStrategy $quoteStrategy;
63
64 /** @var list<PathExpression> */
65 private array $orderByPathExpressions = [];
66
67 /**
68 * We don't want to add path expressions from sub-selects into the select clause of the containing query.
69 * This state flag simply keeps track on whether we are walking on a subquery or not
70 */
71 private bool $inSubSelect = false;
72
73 /**
74 * Stores various parameters that are otherwise unavailable
75 * because Doctrine\ORM\Query\SqlWalker keeps everything private without
76 * accessors.
77 *
78 * {@inheritDoc}
79 */
80 public function __construct(
81 Query $query,
82 ParserResult $parserResult,
83 array $queryComponents,
84 ) {
85 $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
86 $this->rsm = $parserResult->getResultSetMapping();
87
88 // Reset limit and offset
89 $this->firstResult = $query->getFirstResult();
90 $this->maxResults = $query->getMaxResults();
91 $query->setFirstResult(0)->setMaxResults(null);
92
93 $this->em = $query->getEntityManager();
94 $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy();
95
96 parent::__construct($query, $parserResult, $queryComponents);
97 }
98
99 /**
100 * Check if the platform supports the ROW_NUMBER window function.
101 */
102 private function platformSupportsRowNumber(): bool
103 {
104 return $this->platform instanceof PostgreSQLPlatform
105 || $this->platform instanceof SQLServerPlatform
106 || $this->platform instanceof OraclePlatform
107 || $this->platform instanceof DB2Platform
108 || (method_exists($this->platform, 'supportsRowNumberFunction')
109 && $this->platform->supportsRowNumberFunction());
110 }
111
112 /**
113 * Rebuilds a select statement's order by clause for use in a
114 * ROW_NUMBER() OVER() expression.
115 */
116 private function rebuildOrderByForRowNumber(SelectStatement $AST): void
117 {
118 $orderByClause = $AST->orderByClause;
119 $selectAliasToExpressionMap = [];
120 // Get any aliases that are available for select expressions.
121 foreach ($AST->selectClause->selectExpressions as $selectExpression) {
122 $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
123 }
124
125 // Rebuild string orderby expressions to use the select expression they're referencing
126 foreach ($orderByClause->orderByItems as $orderByItem) {
127 if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
128 $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
129 }
130 }
131
132 $func = new RowNumberOverFunction('dctrn_rownum');
133 $func->orderByClause = $AST->orderByClause;
134 $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
135
136 // No need for an order by clause, we'll order by rownum in the outer query.
137 $AST->orderByClause = null;
138 }
139
140 public function walkSelectStatement(SelectStatement $selectStatement): string
141 {
142 if ($this->platformSupportsRowNumber()) {
143 return $this->walkSelectStatementWithRowNumber($selectStatement);
144 }
145
146 return $this->walkSelectStatementWithoutRowNumber($selectStatement);
147 }
148
149 /**
150 * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
151 * This method is for use with platforms which support ROW_NUMBER.
152 *
153 * @throws RuntimeException
154 */
155 public function walkSelectStatementWithRowNumber(SelectStatement $AST): string
156 {
157 $hasOrderBy = false;
158 $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
159 $orderGroupBy = '';
160 if ($AST->orderByClause instanceof OrderByClause) {
161 $hasOrderBy = true;
162 $this->rebuildOrderByForRowNumber($AST);
163 }
164
165 $innerSql = $this->getInnerSQL($AST);
166
167 $sqlIdentifier = $this->getSQLIdentifier($AST);
168
169 if ($hasOrderBy) {
170 $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
171 $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
172 }
173
174 // Build the counter query
175 $sql = sprintf(
176 'SELECT DISTINCT %s FROM (%s) dctrn_result',
177 implode(', ', $sqlIdentifier),
178 $innerSql,
179 );
180
181 if ($hasOrderBy) {
182 $sql .= $orderGroupBy . $outerOrderBy;
183 }
184
185 // Apply the limit and offset.
186 $sql = $this->platform->modifyLimitQuery(
187 $sql,
188 $this->maxResults,
189 $this->firstResult,
190 );
191
192 // Add the columns to the ResultSetMapping. It's not really nice but
193 // it works. Preferably I'd clear the RSM or simply create a new one
194 // but that is not possible from inside the output walker, so we dirty
195 // up the one we have.
196 foreach ($sqlIdentifier as $property => $alias) {
197 $this->rsm->addScalarResult($alias, $property);
198 }
199
200 return $sql;
201 }
202
203 /**
204 * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
205 * This method is for platforms which DO NOT support ROW_NUMBER.
206 *
207 * @throws RuntimeException
208 */
209 public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string
210 {
211 // We don't want to call this recursively!
212 if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
213 // In the case of ordering a query by columns from joined tables, we
214 // must add those columns to the select clause of the query BEFORE
215 // the SQL is generated.
216 $this->addMissingItemsFromOrderByToSelect($AST);
217 }
218
219 // Remove order by clause from the inner query
220 // It will be re-appended in the outer select generated by this method
221 $orderByClause = $AST->orderByClause;
222 $AST->orderByClause = null;
223
224 $innerSql = $this->getInnerSQL($AST);
225
226 $sqlIdentifier = $this->getSQLIdentifier($AST);
227
228 // Build the counter query
229 $sql = sprintf(
230 'SELECT DISTINCT %s FROM (%s) dctrn_result',
231 implode(', ', $sqlIdentifier),
232 $innerSql,
233 );
234
235 // https://github.com/doctrine/orm/issues/2630
236 $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
237
238 // Apply the limit and offset.
239 $sql = $this->platform->modifyLimitQuery(
240 $sql,
241 $this->maxResults,
242 $this->firstResult,
243 );
244
245 // Add the columns to the ResultSetMapping. It's not really nice but
246 // it works. Preferably I'd clear the RSM or simply create a new one
247 // but that is not possible from inside the output walker, so we dirty
248 // up the one we have.
249 foreach ($sqlIdentifier as $property => $alias) {
250 $this->rsm->addScalarResult($alias, $property);
251 }
252
253 // Restore orderByClause
254 $AST->orderByClause = $orderByClause;
255
256 return $sql;
257 }
258
259 /**
260 * Finds all PathExpressions in an AST's OrderByClause, and ensures that
261 * the referenced fields are present in the SelectClause of the passed AST.
262 */
263 private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void
264 {
265 $this->orderByPathExpressions = [];
266
267 // We need to do this in another walker because otherwise we'll end up
268 // polluting the state of this one.
269 $walker = clone $this;
270
271 // This will populate $orderByPathExpressions via
272 // LimitSubqueryOutputWalker::walkPathExpression, which will be called
273 // as the select statement is walked. We'll end up with an array of all
274 // path expressions referenced in the query.
275 $walker->walkSelectStatementWithoutRowNumber($AST, false);
276 $orderByPathExpressions = $walker->getOrderByPathExpressions();
277
278 // Get a map of referenced identifiers to field names.
279 $selects = [];
280 foreach ($orderByPathExpressions as $pathExpression) {
281 assert($pathExpression->field !== null);
282 $idVar = $pathExpression->identificationVariable;
283 $field = $pathExpression->field;
284 if (! isset($selects[$idVar])) {
285 $selects[$idVar] = [];
286 }
287
288 $selects[$idVar][$field] = true;
289 }
290
291 // Loop the select clause of the AST and exclude items from $select
292 // that are already being selected in the query.
293 foreach ($AST->selectClause->selectExpressions as $selectExpression) {
294 if ($selectExpression instanceof SelectExpression) {
295 $idVar = $selectExpression->expression;
296 if (! is_string($idVar)) {
297 continue;
298 }
299
300 $field = $selectExpression->fieldIdentificationVariable;
301 if ($field === null) {
302 // No need to add this select, as we're already fetching the whole object.
303 unset($selects[$idVar]);
304 } else {
305 unset($selects[$idVar][$field]);
306 }
307 }
308 }
309
310 // Add select items which were not excluded to the AST's select clause.
311 foreach ($selects as $idVar => $fields) {
312 $AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true);
313 }
314 }
315
316 /**
317 * Generates new SQL for statements with an order by clause
318 *
319 * @param mixed[] $sqlIdentifier
320 */
321 private function preserveSqlOrdering(
322 array $sqlIdentifier,
323 string $innerSql,
324 string $sql,
325 OrderByClause|null $orderByClause,
326 ): string {
327 // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
328 if (! $orderByClause) {
329 return $sql;
330 }
331
332 // now only select distinct identifier
333 return sprintf(
334 'SELECT DISTINCT %s FROM (%s) dctrn_result',
335 implode(', ', $sqlIdentifier),
336 $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql),
337 );
338 }
339
340 /**
341 * Generates a new SQL statement for the inner query to keep the correct sorting
342 *
343 * @param mixed[] $identifiers
344 */
345 private function recreateInnerSql(
346 OrderByClause $orderByClause,
347 array $identifiers,
348 string $innerSql,
349 ): string {
350 [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
351 $orderByItems = [];
352
353 foreach ($orderByClause->orderByItems as $orderByItem) {
354 // Walk order by item to get string representation of it and
355 // replace path expressions in the order by clause with their column alias
356 $orderByItemString = preg_replace(
357 $searchPatterns,
358 $replacements,
359 $this->walkOrderByItem($orderByItem),
360 );
361
362 $orderByItems[] = $orderByItemString;
363 $identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' '));
364
365 if (! in_array($identifier, $identifiers, true)) {
366 $identifiers[] = $identifier;
367 }
368 }
369
370 return $sql = sprintf(
371 'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
372 implode(', ', $identifiers),
373 $innerSql,
374 implode(', ', $orderByItems),
375 );
376 }
377
378 /**
379 * @return string[][]
380 * @psalm-return array{0: list<non-empty-string>, 1: list<string>}
381 */
382 private function generateSqlAliasReplacements(): array
383 {
384 $aliasMap = $searchPatterns = $replacements = $metadataList = [];
385
386 // Generate DQL alias -> SQL table alias mapping
387 foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) {
388 $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias);
389 $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
390 }
391
392 // Generate search patterns for each field's path expression in the order by clause
393 foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
394 $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
395 $class = $metadataList[$dqlAliasForFieldAlias];
396
397 // If the field is from a joined child table, we won't be ordering on it.
398 if (! isset($class->fieldMappings[$fieldName])) {
399 continue;
400 }
401
402 $fieldMapping = $class->fieldMappings[$fieldName];
403
404 // Get the proper column name as will appear in the select list
405 $columnName = $this->quoteStrategy->getColumnName(
406 $fieldName,
407 $metadataList[$dqlAliasForFieldAlias],
408 $this->em->getConnection()->getDatabasePlatform(),
409 );
410
411 // Get the SQL table alias for the entity and field
412 $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias];
413
414 if (isset($fieldMapping->declared) && $fieldMapping->declared !== $class->name) {
415 // Field was declared in a parent class, so we need to get the proper SQL table alias
416 // for the joined parent table.
417 $otherClassMetadata = $this->em->getClassMetadata($fieldMapping->declared);
418
419 if (! $otherClassMetadata->isMappedSuperclass) {
420 $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
421 }
422 }
423
424 // Compose search and replace patterns
425 $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName);
426 $replacements[] = $fieldAlias;
427 }
428
429 return [$searchPatterns, $replacements];
430 }
431
432 /**
433 * getter for $orderByPathExpressions
434 *
435 * @return list<PathExpression>
436 */
437 public function getOrderByPathExpressions(): array
438 {
439 return $this->orderByPathExpressions;
440 }
441
442 /**
443 * @throws OptimisticLockException
444 * @throws QueryException
445 */
446 private function getInnerSQL(SelectStatement $AST): string
447 {
448 // Set every select expression as visible(hidden = false) to
449 // make $AST have scalar mappings properly - this is relevant for referencing selected
450 // fields from outside the subquery, for example in the ORDER BY segment
451 $hiddens = [];
452
453 foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
454 $hiddens[$idx] = $expr->hiddenAliasResultVariable;
455 $expr->hiddenAliasResultVariable = false;
456 }
457
458 $innerSql = parent::walkSelectStatement($AST);
459
460 // Restore hiddens
461 foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
462 $expr->hiddenAliasResultVariable = $hiddens[$idx];
463 }
464
465 return $innerSql;
466 }
467
468 /** @return string[] */
469 private function getSQLIdentifier(SelectStatement $AST): array
470 {
471 // Find out the SQL alias of the identifier column of the root entity.
472 // It may be possible to make this work with multiple root entities but that
473 // would probably require issuing multiple queries or doing a UNION SELECT.
474 // So for now, it's not supported.
475
476 // Get the root entity and alias from the AST fromClause.
477 $from = $AST->fromClause->identificationVariableDeclarations;
478 if (count($from) !== 1) {
479 throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
480 }
481
482 $fromRoot = reset($from);
483 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
484 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
485 $rootIdentifier = $rootClass->identifier;
486
487 // For every identifier, find out the SQL alias by combing through the ResultSetMapping
488 $sqlIdentifier = [];
489 foreach ($rootIdentifier as $property) {
490 if (isset($rootClass->fieldMappings[$property])) {
491 foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) {
492 if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
493 $sqlIdentifier[$property] = $alias;
494 }
495 }
496 }
497
498 if (isset($rootClass->associationMappings[$property])) {
499 $association = $rootClass->associationMappings[$property];
500 assert($association->isToOneOwningSide());
501 $joinColumn = $association->joinColumns[0]->name;
502
503 foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) {
504 if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
505 $sqlIdentifier[$property] = $alias;
506 }
507 }
508 }
509 }
510
511 if (count($sqlIdentifier) === 0) {
512 throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
513 }
514
515 if (count($rootIdentifier) !== count($sqlIdentifier)) {
516 throw new RuntimeException(sprintf(
517 'Not all identifier properties can be found in the ResultSetMapping: %s',
518 implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))),
519 ));
520 }
521
522 return $sqlIdentifier;
523 }
524
525 public function walkPathExpression(PathExpression $pathExpr): string
526 {
527 if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) {
528 $this->orderByPathExpressions[] = $pathExpr;
529 }
530
531 return parent::walkPathExpression($pathExpr);
532 }
533
534 public function walkSubSelect(Subselect $subselect): string
535 {
536 $this->inSubSelect = true;
537
538 $sql = parent::walkSubselect($subselect);
539
540 $this->inSubSelect = false;
541
542 return $sql;
543 }
544}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php
new file mode 100644
index 0000000..3fb0eee
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\ORM\Query;
9use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
10use Doctrine\ORM\Query\AST\Node;
11use Doctrine\ORM\Query\AST\PathExpression;
12use Doctrine\ORM\Query\AST\SelectExpression;
13use Doctrine\ORM\Query\AST\SelectStatement;
14use Doctrine\ORM\Query\TreeWalkerAdapter;
15use RuntimeException;
16
17use function count;
18use function is_string;
19use function reset;
20
21/**
22 * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
23 */
24class LimitSubqueryWalker extends TreeWalkerAdapter
25{
26 public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type';
27
28 public const FORCE_DBAL_TYPE_CONVERSION = 'doctrine_paginator.scalar_result.force_dbal_type_conversion';
29
30 /**
31 * Counter for generating unique order column aliases.
32 */
33 private int $aliasCounter = 0;
34
35 public function walkSelectStatement(SelectStatement $selectStatement): void
36 {
37 // Get the root entity and alias from the AST fromClause
38 $from = $selectStatement->fromClause->identificationVariableDeclarations;
39 $fromRoot = reset($from);
40 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
41 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
42
43 $this->validate($selectStatement);
44 $identifier = $rootClass->getSingleIdentifierFieldName();
45
46 if (isset($rootClass->associationMappings[$identifier])) {
47 throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.');
48 }
49
50 $query = $this->_getQuery();
51
52 $query->setHint(
53 self::IDENTIFIER_TYPE,
54 Type::getType($rootClass->fieldMappings[$identifier]->type),
55 );
56
57 $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
58
59 $pathExpression = new PathExpression(
60 PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
61 $rootAlias,
62 $identifier,
63 );
64
65 $pathExpression->type = PathExpression::TYPE_STATE_FIELD;
66
67 $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
68 $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true;
69
70 if (! isset($selectStatement->orderByClause)) {
71 return;
72 }
73
74 $queryComponents = $this->getQueryComponents();
75 foreach ($selectStatement->orderByClause->orderByItems as $item) {
76 if ($item->expression instanceof PathExpression) {
77 $selectStatement->selectClause->selectExpressions[] = new SelectExpression(
78 $this->createSelectExpressionItem($item->expression),
79 '_dctrn_ord' . $this->aliasCounter++,
80 );
81
82 continue;
83 }
84
85 if (is_string($item->expression) && isset($queryComponents[$item->expression])) {
86 $qComp = $queryComponents[$item->expression];
87
88 if (isset($qComp['resultVariable'])) {
89 $selectStatement->selectClause->selectExpressions[] = new SelectExpression(
90 $qComp['resultVariable'],
91 $item->expression,
92 );
93 }
94 }
95 }
96 }
97
98 /**
99 * Validate the AST to ensure that this walker is able to properly manipulate it.
100 */
101 private function validate(SelectStatement $AST): void
102 {
103 // Prevent LimitSubqueryWalker from being used with queries that include
104 // a limit, a fetched to-many join, and an order by condition that
105 // references a column from the fetch joined table.
106 $queryComponents = $this->getQueryComponents();
107 $query = $this->_getQuery();
108 $from = $AST->fromClause->identificationVariableDeclarations;
109 $fromRoot = reset($from);
110
111 if (
112 $query instanceof Query
113 && $query->getMaxResults() !== null
114 && $AST->orderByClause
115 && count($fromRoot->joins)
116 ) {
117 // Check each orderby item.
118 // TODO: check complex orderby items too...
119 foreach ($AST->orderByClause->orderByItems as $orderByItem) {
120 $expression = $orderByItem->expression;
121 if (
122 $orderByItem->expression instanceof PathExpression
123 && isset($queryComponents[$expression->identificationVariable])
124 ) {
125 $queryComponent = $queryComponents[$expression->identificationVariable];
126 if (
127 isset($queryComponent['parent'])
128 && isset($queryComponent['relation'])
129 && $queryComponent['relation']->isToMany()
130 ) {
131 throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.');
132 }
133 }
134 }
135 }
136 }
137
138 /**
139 * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name).
140 *
141 * @return IdentityFunction|PathExpression
142 */
143 private function createSelectExpressionItem(PathExpression $pathExpression): Node
144 {
145 if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
146 $identity = new IdentityFunction('identity');
147
148 $identity->pathExpression = clone $pathExpression;
149
150 return $identity;
151 }
152
153 return clone $pathExpression;
154 }
155}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php
new file mode 100644
index 0000000..db1b34d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php
@@ -0,0 +1,263 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use ArrayIterator;
8use Countable;
9use Doctrine\Common\Collections\Collection;
10use Doctrine\ORM\Internal\SQLResultCasing;
11use Doctrine\ORM\NoResultException;
12use Doctrine\ORM\Query;
13use Doctrine\ORM\Query\Parameter;
14use Doctrine\ORM\Query\Parser;
15use Doctrine\ORM\Query\ResultSetMapping;
16use Doctrine\ORM\QueryBuilder;
17use IteratorAggregate;
18use Traversable;
19
20use function array_key_exists;
21use function array_map;
22use function array_sum;
23use function assert;
24use function is_string;
25
26/**
27 * The paginator can handle various complex scenarios with DQL.
28 *
29 * @template-covariant T
30 * @implements IteratorAggregate<array-key, T>
31 */
32class Paginator implements Countable, IteratorAggregate
33{
34 use SQLResultCasing;
35
36 public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable';
37
38 private readonly Query $query;
39 private bool|null $useOutputWalkers = null;
40 private int|null $count = null;
41
42 /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */
43 public function __construct(
44 Query|QueryBuilder $query,
45 private readonly bool $fetchJoinCollection = true,
46 ) {
47 if ($query instanceof QueryBuilder) {
48 $query = $query->getQuery();
49 }
50
51 $this->query = $query;
52 }
53
54 /**
55 * Returns the query.
56 */
57 public function getQuery(): Query
58 {
59 return $this->query;
60 }
61
62 /**
63 * Returns whether the query joins a collection.
64 *
65 * @return bool Whether the query joins a collection.
66 */
67 public function getFetchJoinCollection(): bool
68 {
69 return $this->fetchJoinCollection;
70 }
71
72 /**
73 * Returns whether the paginator will use an output walker.
74 */
75 public function getUseOutputWalkers(): bool|null
76 {
77 return $this->useOutputWalkers;
78 }
79
80 /**
81 * Sets whether the paginator will use an output walker.
82 *
83 * @return $this
84 */
85 public function setUseOutputWalkers(bool|null $useOutputWalkers): static
86 {
87 $this->useOutputWalkers = $useOutputWalkers;
88
89 return $this;
90 }
91
92 public function count(): int
93 {
94 if ($this->count === null) {
95 try {
96 $this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult()));
97 } catch (NoResultException) {
98 $this->count = 0;
99 }
100 }
101
102 return $this->count;
103 }
104
105 /**
106 * {@inheritDoc}
107 *
108 * @psalm-return Traversable<array-key, T>
109 */
110 public function getIterator(): Traversable
111 {
112 $offset = $this->query->getFirstResult();
113 $length = $this->query->getMaxResults();
114
115 if ($this->fetchJoinCollection && $length !== null) {
116 $subQuery = $this->cloneQuery($this->query);
117
118 if ($this->useOutputWalker($subQuery)) {
119 $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
120 } else {
121 $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class);
122 $this->unbindUnusedQueryParams($subQuery);
123 }
124
125 $subQuery->setFirstResult($offset)->setMaxResults($length);
126
127 $foundIdRows = $subQuery->getScalarResult();
128
129 // don't do this for an empty id array
130 if ($foundIdRows === []) {
131 return new ArrayIterator([]);
132 }
133
134 $whereInQuery = $this->cloneQuery($this->query);
135 $ids = array_map('current', $foundIdRows);
136
137 $this->appendTreeWalker($whereInQuery, WhereInWalker::class);
138 $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true);
139 $whereInQuery->setFirstResult(0)->setMaxResults(null);
140 $whereInQuery->setCacheable($this->query->isCacheable());
141
142 $databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids);
143 $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds);
144
145 $result = $whereInQuery->getResult($this->query->getHydrationMode());
146 } else {
147 $result = $this->cloneQuery($this->query)
148 ->setMaxResults($length)
149 ->setFirstResult($offset)
150 ->setCacheable($this->query->isCacheable())
151 ->getResult($this->query->getHydrationMode());
152 }
153
154 return new ArrayIterator($result);
155 }
156
157 private function cloneQuery(Query $query): Query
158 {
159 $cloneQuery = clone $query;
160
161 $cloneQuery->setParameters(clone $query->getParameters());
162 $cloneQuery->setCacheable(false);
163
164 foreach ($query->getHints() as $name => $value) {
165 $cloneQuery->setHint($name, $value);
166 }
167
168 return $cloneQuery;
169 }
170
171 /**
172 * Determines whether to use an output walker for the query.
173 */
174 private function useOutputWalker(Query $query): bool
175 {
176 if ($this->useOutputWalkers === null) {
177 return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false;
178 }
179
180 return $this->useOutputWalkers;
181 }
182
183 /**
184 * Appends a custom tree walker to the tree walkers hint.
185 *
186 * @psalm-param class-string $walkerClass
187 */
188 private function appendTreeWalker(Query $query, string $walkerClass): void
189 {
190 $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
191
192 if ($hints === false) {
193 $hints = [];
194 }
195
196 $hints[] = $walkerClass;
197 $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints);
198 }
199
200 /**
201 * Returns Query prepared to count.
202 */
203 private function getCountQuery(): Query
204 {
205 $countQuery = $this->cloneQuery($this->query);
206
207 if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) {
208 $countQuery->setHint(CountWalker::HINT_DISTINCT, true);
209 }
210
211 if ($this->useOutputWalker($countQuery)) {
212 $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win
213
214 $rsm = new ResultSetMapping();
215 $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count');
216
217 $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class);
218 $countQuery->setResultSetMapping($rsm);
219 } else {
220 $this->appendTreeWalker($countQuery, CountWalker::class);
221 $this->unbindUnusedQueryParams($countQuery);
222 }
223
224 $countQuery->setFirstResult(0)->setMaxResults(null);
225
226 return $countQuery;
227 }
228
229 private function unbindUnusedQueryParams(Query $query): void
230 {
231 $parser = new Parser($query);
232 $parameterMappings = $parser->parse()->getParameterMappings();
233 /** @var Collection|Parameter[] $parameters */
234 $parameters = $query->getParameters();
235
236 foreach ($parameters as $key => $parameter) {
237 $parameterName = $parameter->getName();
238
239 if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) {
240 unset($parameters[$key]);
241 }
242 }
243
244 $query->setParameters($parameters);
245 }
246
247 /**
248 * @param mixed[] $identifiers
249 *
250 * @return mixed[]
251 */
252 private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array
253 {
254 $query = $this->cloneQuery($this->query);
255 $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class);
256
257 $connection = $this->query->getEntityManager()->getConnection();
258 $type = $query->getSQL();
259 assert(is_string($type));
260
261 return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id, $type), $identifiers);
262 }
263}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php
new file mode 100644
index 0000000..f630ee1
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php
@@ -0,0 +1,48 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST;
8use Doctrine\ORM\Query\SqlWalker;
9use Doctrine\ORM\Utility\PersisterHelper;
10use RuntimeException;
11
12use function count;
13use function reset;
14
15/**
16 * Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and
17 * returns it in place of a real SQL statement.
18 *
19 * Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator.
20 * We can best do this from a tree walker because it gives us access to the AST.
21 *
22 * Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
23 * benefit that the DQL -> root entity id type resolution can be cached in the query cache.
24 */
25final class RootTypeWalker extends SqlWalker
26{
27 public function walkSelectStatement(AST\SelectStatement $selectStatement): string
28 {
29 // Get the root entity and alias from the AST fromClause
30 $from = $selectStatement->fromClause->identificationVariableDeclarations;
31
32 if (count($from) > 1) {
33 throw new RuntimeException('Can only process queries that select only one FROM component');
34 }
35
36 $fromRoot = reset($from);
37 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
38 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
39 $identifierFieldName = $rootClass->getSingleIdentifierFieldName();
40
41 return PersisterHelper::getTypeOfField(
42 $identifierFieldName,
43 $rootClass,
44 $this->getQuery()
45 ->getEntityManager(),
46 )[0];
47 }
48}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php
new file mode 100644
index 0000000..a0fdd01
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\Functions\FunctionNode;
8use Doctrine\ORM\Query\AST\OrderByClause;
9use Doctrine\ORM\Query\Parser;
10use Doctrine\ORM\Query\SqlWalker;
11use Doctrine\ORM\Tools\Pagination\Exception\RowNumberOverFunctionNotEnabled;
12
13use function trim;
14
15/**
16 * RowNumberOverFunction
17 *
18 * Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker
19 */
20class RowNumberOverFunction extends FunctionNode
21{
22 public OrderByClause $orderByClause;
23
24 public function getSql(SqlWalker $sqlWalker): string
25 {
26 return 'ROW_NUMBER() OVER(' . trim($sqlWalker->walkOrderByClause(
27 $this->orderByClause,
28 )) . ')';
29 }
30
31 /**
32 * @throws RowNumberOverFunctionNotEnabled
33 *
34 * @inheritdoc
35 */
36 public function parse(Parser $parser): void
37 {
38 throw RowNumberOverFunctionNotEnabled::create();
39 }
40}
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php
new file mode 100644
index 0000000..01741ca
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php
@@ -0,0 +1,116 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\ORM\Query\AST\ArithmeticExpression;
8use Doctrine\ORM\Query\AST\ConditionalExpression;
9use Doctrine\ORM\Query\AST\ConditionalPrimary;
10use Doctrine\ORM\Query\AST\ConditionalTerm;
11use Doctrine\ORM\Query\AST\InListExpression;
12use Doctrine\ORM\Query\AST\InputParameter;
13use Doctrine\ORM\Query\AST\NullComparisonExpression;
14use Doctrine\ORM\Query\AST\PathExpression;
15use Doctrine\ORM\Query\AST\SelectStatement;
16use Doctrine\ORM\Query\AST\SimpleArithmeticExpression;
17use Doctrine\ORM\Query\AST\WhereClause;
18use Doctrine\ORM\Query\TreeWalkerAdapter;
19use RuntimeException;
20
21use function count;
22use function reset;
23
24/**
25 * Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST.
26 *
27 * The parameter namespace (dpid) is defined by
28 * the PAGINATOR_ID_ALIAS
29 *
30 * The HINT_PAGINATOR_HAS_IDS query hint indicates whether there are
31 * any ids in the parameter at all.
32 */
33class WhereInWalker extends TreeWalkerAdapter
34{
35 /**
36 * ID Count hint name.
37 */
38 public const HINT_PAGINATOR_HAS_IDS = 'doctrine.paginator_has_ids';
39
40 /**
41 * Primary key alias for query.
42 */
43 public const PAGINATOR_ID_ALIAS = 'dpid';
44
45 public function walkSelectStatement(SelectStatement $selectStatement): void
46 {
47 // Get the root entity and alias from the AST fromClause
48 $from = $selectStatement->fromClause->identificationVariableDeclarations;
49
50 if (count($from) > 1) {
51 throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
52 }
53
54 $fromRoot = reset($from);
55 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
56 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
57 $identifierFieldName = $rootClass->getSingleIdentifierFieldName();
58
59 $pathType = PathExpression::TYPE_STATE_FIELD;
60 if (isset($rootClass->associationMappings[$identifierFieldName])) {
61 $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
62 }
63
64 $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName);
65 $pathExpression->type = $pathType;
66
67 $hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS);
68
69 if ($hasIds) {
70 $arithmeticExpression = new ArithmeticExpression();
71 $arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression(
72 [$pathExpression],
73 );
74 $expression = new InListExpression(
75 $arithmeticExpression,
76 [new InputParameter(':' . self::PAGINATOR_ID_ALIAS)],
77 );
78 } else {
79 $expression = new NullComparisonExpression($pathExpression);
80 }
81
82 $conditionalPrimary = new ConditionalPrimary();
83 $conditionalPrimary->simpleConditionalExpression = $expression;
84 if ($selectStatement->whereClause) {
85 if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) {
86 $selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary;
87 } elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) {
88 $selectStatement->whereClause->conditionalExpression = new ConditionalExpression(
89 [
90 new ConditionalTerm(
91 [
92 $selectStatement->whereClause->conditionalExpression,
93 $conditionalPrimary,
94 ],
95 ),
96 ],
97 );
98 } else {
99 $tmpPrimary = new ConditionalPrimary();
100 $tmpPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression;
101 $selectStatement->whereClause->conditionalExpression = new ConditionalTerm(
102 [
103 $tmpPrimary,
104 $conditionalPrimary,
105 ],
106 );
107 }
108 } else {
109 $selectStatement->whereClause = new WhereClause(
110 new ConditionalExpression(
111 [new ConditionalTerm([$conditionalPrimary])],
112 ),
113 );
114 }
115 }
116}
diff --git a/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php
new file mode 100644
index 0000000..9e48521
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php
@@ -0,0 +1,117 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\Common\EventSubscriber;
8use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
9use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
10use Doctrine\ORM\Events;
11use Doctrine\ORM\Mapping\AssociationMapping;
12use Doctrine\ORM\Mapping\ClassMetadata;
13
14use function array_key_exists;
15use function array_replace_recursive;
16use function ltrim;
17
18/**
19 * ResolveTargetEntityListener
20 *
21 * Mechanism to overwrite interfaces or classes specified as association
22 * targets.
23 */
24class ResolveTargetEntityListener implements EventSubscriber
25{
26 /** @var mixed[][] indexed by original entity name */
27 private array $resolveTargetEntities = [];
28
29 /**
30 * {@inheritDoc}
31 */
32 public function getSubscribedEvents(): array
33 {
34 return [
35 Events::loadClassMetadata,
36 Events::onClassMetadataNotFound,
37 ];
38 }
39
40 /**
41 * Adds a target-entity class name to resolve to a new class name.
42 *
43 * @psalm-param array<string, mixed> $mapping
44 */
45 public function addResolveTargetEntity(string $originalEntity, string $newEntity, array $mapping): void
46 {
47 $mapping['targetEntity'] = ltrim($newEntity, '\\');
48 $this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping;
49 }
50
51 /** @internal this is an event callback, and should not be called directly */
52 public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args): void
53 {
54 if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) {
55 $args->setFoundMetadata(
56 $args
57 ->getObjectManager()
58 ->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity']),
59 );
60 }
61 }
62
63 /**
64 * Processes event and resolves new target entity names.
65 *
66 * @internal this is an event callback, and should not be called directly
67 */
68 public function loadClassMetadata(LoadClassMetadataEventArgs $args): void
69 {
70 $cm = $args->getClassMetadata();
71
72 foreach ($cm->associationMappings as $mapping) {
73 if (isset($this->resolveTargetEntities[$mapping->targetEntity])) {
74 $this->remapAssociation($cm, $mapping);
75 }
76 }
77
78 foreach ($this->resolveTargetEntities as $interface => $data) {
79 if ($data['targetEntity'] === $cm->getName()) {
80 $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm);
81 }
82 }
83
84 foreach ($cm->discriminatorMap as $value => $class) {
85 if (isset($this->resolveTargetEntities[$class])) {
86 $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']);
87 }
88 }
89 }
90
91 private function remapAssociation(ClassMetadata $classMetadata, AssociationMapping $mapping): void
92 {
93 $newMapping = $this->resolveTargetEntities[$mapping->targetEntity];
94 $newMapping = array_replace_recursive(
95 $mapping->toArray(),
96 $newMapping,
97 );
98 $newMapping['fieldName'] = $mapping->fieldName;
99
100 unset($classMetadata->associationMappings[$mapping->fieldName]);
101
102 switch ($mapping->type()) {
103 case ClassMetadata::MANY_TO_MANY:
104 $classMetadata->mapManyToMany($newMapping);
105 break;
106 case ClassMetadata::MANY_TO_ONE:
107 $classMetadata->mapManyToOne($newMapping);
108 break;
109 case ClassMetadata::ONE_TO_MANY:
110 $classMetadata->mapOneToMany($newMapping);
111 break;
112 case ClassMetadata::ONE_TO_ONE:
113 $classMetadata->mapOneToOne($newMapping);
114 break;
115 }
116 }
117}
diff --git a/vendor/doctrine/orm/src/Tools/SchemaTool.php b/vendor/doctrine/orm/src/Tools/SchemaTool.php
new file mode 100644
index 0000000..42b52df
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/SchemaTool.php
@@ -0,0 +1,932 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use BackedEnum;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Schema\AbstractAsset;
10use Doctrine\DBAL\Schema\AbstractSchemaManager;
11use Doctrine\DBAL\Schema\Index;
12use Doctrine\DBAL\Schema\Schema;
13use Doctrine\DBAL\Schema\Table;
14use Doctrine\ORM\EntityManagerInterface;
15use Doctrine\ORM\Mapping\AssociationMapping;
16use Doctrine\ORM\Mapping\ClassMetadata;
17use Doctrine\ORM\Mapping\DiscriminatorColumnMapping;
18use Doctrine\ORM\Mapping\FieldMapping;
19use Doctrine\ORM\Mapping\JoinColumnMapping;
20use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
21use Doctrine\ORM\Mapping\MappingException;
22use Doctrine\ORM\Mapping\QuoteStrategy;
23use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
24use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
25use Doctrine\ORM\Tools\Exception\MissingColumnException;
26use Doctrine\ORM\Tools\Exception\NotSupported;
27use Throwable;
28
29use function array_diff;
30use function array_diff_key;
31use function array_filter;
32use function array_flip;
33use function array_intersect_key;
34use function assert;
35use function count;
36use function current;
37use function implode;
38use function in_array;
39use function is_numeric;
40use function strtolower;
41
42/**
43 * The SchemaTool is a tool to create/drop/update database schemas based on
44 * <tt>ClassMetadata</tt> class descriptors.
45 *
46 * @link www.doctrine-project.org
47 */
48class SchemaTool
49{
50 private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
51
52 private readonly AbstractPlatform $platform;
53 private readonly QuoteStrategy $quoteStrategy;
54 private readonly AbstractSchemaManager $schemaManager;
55
56 /**
57 * Initializes a new SchemaTool instance that uses the connection of the
58 * provided EntityManager.
59 */
60 public function __construct(private readonly EntityManagerInterface $em)
61 {
62 $this->platform = $em->getConnection()->getDatabasePlatform();
63 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
64 $this->schemaManager = $em->getConnection()->createSchemaManager();
65 }
66
67 /**
68 * Creates the database schema for the given array of ClassMetadata instances.
69 *
70 * @psalm-param list<ClassMetadata> $classes
71 *
72 * @throws ToolsException
73 */
74 public function createSchema(array $classes): void
75 {
76 $createSchemaSql = $this->getCreateSchemaSql($classes);
77 $conn = $this->em->getConnection();
78
79 foreach ($createSchemaSql as $sql) {
80 try {
81 $conn->executeStatement($sql);
82 } catch (Throwable $e) {
83 throw ToolsException::schemaToolFailure($sql, $e);
84 }
85 }
86 }
87
88 /**
89 * Gets the list of DDL statements that are required to create the database schema for
90 * the given list of ClassMetadata instances.
91 *
92 * @psalm-param list<ClassMetadata> $classes
93 *
94 * @return list<string> The SQL statements needed to create the schema for the classes.
95 */
96 public function getCreateSchemaSql(array $classes): array
97 {
98 $schema = $this->getSchemaFromMetadata($classes);
99
100 return $schema->toSql($this->platform);
101 }
102
103 /**
104 * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
105 *
106 * @psalm-param array<string, bool> $processedClasses
107 */
108 private function processingNotRequired(
109 ClassMetadata $class,
110 array $processedClasses,
111 ): bool {
112 return isset($processedClasses[$class->name]) ||
113 $class->isMappedSuperclass ||
114 $class->isEmbeddedClass ||
115 ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) ||
116 in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses());
117 }
118
119 /**
120 * Resolves fields in index mapping to column names
121 *
122 * @param mixed[] $indexData index or unique constraint data
123 *
124 * @return list<string> Column names from combined fields and columns mappings
125 */
126 private function getIndexColumns(ClassMetadata $class, array $indexData): array
127 {
128 $columns = [];
129
130 if (
131 isset($indexData['columns'], $indexData['fields'])
132 || (
133 ! isset($indexData['columns'])
134 && ! isset($indexData['fields'])
135 )
136 ) {
137 throw MappingException::invalidIndexConfiguration(
138 (string) $class,
139 $indexData['name'] ?? 'unnamed',
140 );
141 }
142
143 if (isset($indexData['columns'])) {
144 $columns = $indexData['columns'];
145 }
146
147 if (isset($indexData['fields'])) {
148 foreach ($indexData['fields'] as $fieldName) {
149 if ($class->hasField($fieldName)) {
150 $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
151 } elseif ($class->hasAssociation($fieldName)) {
152 $assoc = $class->getAssociationMapping($fieldName);
153 assert($assoc->isToOneOwningSide());
154 foreach ($assoc->joinColumns as $joinColumn) {
155 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
156 }
157 }
158 }
159 }
160
161 return $columns;
162 }
163
164 /**
165 * Creates a Schema instance from a given set of metadata classes.
166 *
167 * @psalm-param list<ClassMetadata> $classes
168 *
169 * @throws NotSupported
170 */
171 public function getSchemaFromMetadata(array $classes): Schema
172 {
173 // Reminder for processed classes, used for hierarchies
174 $processedClasses = [];
175 $eventManager = $this->em->getEventManager();
176 $metadataSchemaConfig = $this->schemaManager->createSchemaConfig();
177
178 $schema = new Schema([], [], $metadataSchemaConfig);
179
180 $addedFks = [];
181 $blacklistedFks = [];
182
183 foreach ($classes as $class) {
184 if ($this->processingNotRequired($class, $processedClasses)) {
185 continue;
186 }
187
188 $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
189
190 if ($class->isInheritanceTypeSingleTable()) {
191 $this->gatherColumns($class, $table);
192 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
193
194 // Add the discriminator column
195 $this->addDiscriminatorColumnDefinition($class, $table);
196
197 // Aggregate all the information from all classes in the hierarchy
198 foreach ($class->parentClasses as $parentClassName) {
199 // Parent class information is already contained in this class
200 $processedClasses[$parentClassName] = true;
201 }
202
203 foreach ($class->subClasses as $subClassName) {
204 $subClass = $this->em->getClassMetadata($subClassName);
205 $this->gatherColumns($subClass, $table);
206 $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
207 $processedClasses[$subClassName] = true;
208 }
209 } elseif ($class->isInheritanceTypeJoined()) {
210 // Add all non-inherited fields as columns
211 foreach ($class->fieldMappings as $fieldName => $mapping) {
212 if (! isset($mapping->inherited)) {
213 $this->gatherColumn($class, $mapping, $table);
214 }
215 }
216
217 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
218
219 // Add the discriminator column only to the root table
220 if ($class->name === $class->rootEntityName) {
221 $this->addDiscriminatorColumnDefinition($class, $table);
222 } else {
223 // Add an ID FK column to child tables
224 $pkColumns = [];
225 $inheritedKeyColumns = [];
226
227 foreach ($class->identifier as $identifierField) {
228 if (isset($class->fieldMappings[$identifierField]->inherited)) {
229 $idMapping = $class->fieldMappings[$identifierField];
230 $this->gatherColumn($class, $idMapping, $table);
231 $columnName = $this->quoteStrategy->getColumnName(
232 $identifierField,
233 $class,
234 $this->platform,
235 );
236 // TODO: This seems rather hackish, can we optimize it?
237 $table->getColumn($columnName)->setAutoincrement(false);
238
239 $pkColumns[] = $columnName;
240 $inheritedKeyColumns[] = $columnName;
241
242 continue;
243 }
244
245 if (isset($class->associationMappings[$identifierField]->inherited)) {
246 $idMapping = $class->associationMappings[$identifierField];
247 assert($idMapping->isToOneOwningSide());
248
249 $targetEntity = current(
250 array_filter(
251 $classes,
252 static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity,
253 ),
254 );
255
256 foreach ($idMapping->joinColumns as $joinColumn) {
257 if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) {
258 $columnName = $this->quoteStrategy->getJoinColumnName(
259 $joinColumn,
260 $class,
261 $this->platform,
262 );
263
264 $pkColumns[] = $columnName;
265 $inheritedKeyColumns[] = $columnName;
266 }
267 }
268 }
269 }
270
271 if ($inheritedKeyColumns !== []) {
272 // Add a FK constraint on the ID column
273 $table->addForeignKeyConstraint(
274 $this->quoteStrategy->getTableName(
275 $this->em->getClassMetadata($class->rootEntityName),
276 $this->platform,
277 ),
278 $inheritedKeyColumns,
279 $inheritedKeyColumns,
280 ['onDelete' => 'CASCADE'],
281 );
282 }
283
284 if ($pkColumns !== []) {
285 $table->setPrimaryKey($pkColumns);
286 }
287 }
288 } else {
289 $this->gatherColumns($class, $table);
290 $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
291 }
292
293 $pkColumns = [];
294
295 foreach ($class->identifier as $identifierField) {
296 if (isset($class->fieldMappings[$identifierField])) {
297 $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
298 } elseif (isset($class->associationMappings[$identifierField])) {
299 $assoc = $class->associationMappings[$identifierField];
300 assert($assoc->isToOneOwningSide());
301
302 foreach ($assoc->joinColumns as $joinColumn) {
303 $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
304 }
305 }
306 }
307
308 if (! $table->hasIndex('primary')) {
309 $table->setPrimaryKey($pkColumns);
310 }
311
312 // there can be unique indexes automatically created for join column
313 // if join column is also primary key we should keep only primary key on this column
314 // so, remove indexes overruled by primary key
315 $primaryKey = $table->getIndex('primary');
316
317 foreach ($table->getIndexes() as $idxKey => $existingIndex) {
318 if ($primaryKey->overrules($existingIndex)) {
319 $table->dropIndex($idxKey);
320 }
321 }
322
323 if (isset($class->table['indexes'])) {
324 foreach ($class->table['indexes'] as $indexName => $indexData) {
325 if (! isset($indexData['flags'])) {
326 $indexData['flags'] = [];
327 }
328
329 $table->addIndex(
330 $this->getIndexColumns($class, $indexData),
331 is_numeric($indexName) ? null : $indexName,
332 (array) $indexData['flags'],
333 $indexData['options'] ?? [],
334 );
335 }
336 }
337
338 if (isset($class->table['uniqueConstraints'])) {
339 foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
340 $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
341
342 foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
343 if ($tableIndex->isFulfilledBy($uniqIndex)) {
344 $table->dropIndex($tableIndexName);
345 break;
346 }
347 }
348
349 $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
350 }
351 }
352
353 if (isset($class->table['options'])) {
354 foreach ($class->table['options'] as $key => $val) {
355 $table->addOption($key, $val);
356 }
357 }
358
359 $processedClasses[$class->name] = true;
360
361 if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) {
362 $seqDef = $class->sequenceGeneratorDefinition;
363 $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
364 if (! $schema->hasSequence($quotedName)) {
365 $schema->createSequence(
366 $quotedName,
367 (int) $seqDef['allocationSize'],
368 (int) $seqDef['initialValue'],
369 );
370 }
371 }
372
373 if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
374 $eventManager->dispatchEvent(
375 ToolEvents::postGenerateSchemaTable,
376 new GenerateSchemaTableEventArgs($class, $schema, $table),
377 );
378 }
379 }
380
381 if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
382 $eventManager->dispatchEvent(
383 ToolEvents::postGenerateSchema,
384 new GenerateSchemaEventArgs($this->em, $schema),
385 );
386 }
387
388 return $schema;
389 }
390
391 /**
392 * Gets a portable column definition as required by the DBAL for the discriminator
393 * column of a class.
394 */
395 private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void
396 {
397 $discrColumn = $class->discriminatorColumn;
398 assert($discrColumn !== null);
399
400 if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) {
401 $discrColumn->type = 'string';
402 $discrColumn->length = 255;
403 }
404
405 $options = [
406 'length' => $discrColumn->length ?? null,
407 'notnull' => true,
408 ];
409
410 if (isset($discrColumn->columnDefinition)) {
411 $options['columnDefinition'] = $discrColumn->columnDefinition;
412 }
413
414 $options = $this->gatherColumnOptions($discrColumn) + $options;
415 $table->addColumn($discrColumn->name, $discrColumn->type, $options);
416 }
417
418 /**
419 * Gathers the column definitions as required by the DBAL of all field mappings
420 * found in the given class.
421 */
422 private function gatherColumns(ClassMetadata $class, Table $table): void
423 {
424 $pkColumns = [];
425
426 foreach ($class->fieldMappings as $mapping) {
427 if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) {
428 continue;
429 }
430
431 $this->gatherColumn($class, $mapping, $table);
432
433 if ($class->isIdentifier($mapping->fieldName)) {
434 $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
435 }
436 }
437 }
438
439 /**
440 * Creates a column definition as required by the DBAL from an ORM field mapping definition.
441 *
442 * @param ClassMetadata $class The class that owns the field mapping.
443 * @psalm-param FieldMapping $mapping The field mapping.
444 */
445 private function gatherColumn(
446 ClassMetadata $class,
447 FieldMapping $mapping,
448 Table $table,
449 ): void {
450 $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform);
451 $columnType = $mapping->type;
452
453 $options = [];
454 $options['length'] = $mapping->length ?? null;
455 $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true;
456 if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) {
457 $options['notnull'] = false;
458 }
459
460 $options['platformOptions'] = [];
461 $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName;
462
463 if (strtolower($columnType) === 'string' && $options['length'] === null) {
464 $options['length'] = 255;
465 }
466
467 if (isset($mapping->precision)) {
468 $options['precision'] = $mapping->precision;
469 }
470
471 if (isset($mapping->scale)) {
472 $options['scale'] = $mapping->scale;
473 }
474
475 if (isset($mapping->default)) {
476 $options['default'] = $mapping->default;
477 }
478
479 if (isset($mapping->columnDefinition)) {
480 $options['columnDefinition'] = $mapping->columnDefinition;
481 }
482
483 // the 'default' option can be overwritten here
484 $options = $this->gatherColumnOptions($mapping) + $options;
485
486 if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) {
487 $options['autoincrement'] = true;
488 }
489
490 if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
491 $options['autoincrement'] = false;
492 }
493
494 if ($table->hasColumn($columnName)) {
495 // required in some inheritance scenarios
496 $table->modifyColumn($columnName, $options);
497 } else {
498 $table->addColumn($columnName, $columnType, $options);
499 }
500
501 $isUnique = $mapping->unique ?? false;
502 if ($isUnique) {
503 $table->addUniqueIndex([$columnName]);
504 }
505 }
506
507 /**
508 * Gathers the SQL for properly setting up the relations of the given class.
509 * This includes the SQL for foreign key constraints and join tables.
510 *
511 * @psalm-param array<string, array{
512 * foreignTableName: string,
513 * foreignColumns: list<string>
514 * }> $addedFks
515 * @psalm-param array<string, bool> $blacklistedFks
516 *
517 * @throws NotSupported
518 */
519 private function gatherRelationsSql(
520 ClassMetadata $class,
521 Table $table,
522 Schema $schema,
523 array &$addedFks,
524 array &$blacklistedFks,
525 ): void {
526 foreach ($class->associationMappings as $id => $mapping) {
527 if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) {
528 continue;
529 }
530
531 $foreignClass = $this->em->getClassMetadata($mapping->targetEntity);
532
533 if ($mapping->isToOneOwningSide()) {
534 $primaryKeyColumns = []; // PK is unnecessary for this relation-type
535
536 $this->gatherRelationJoinColumns(
537 $mapping->joinColumns,
538 $table,
539 $foreignClass,
540 $mapping,
541 $primaryKeyColumns,
542 $addedFks,
543 $blacklistedFks,
544 );
545 } elseif ($mapping instanceof ManyToManyOwningSideMapping) {
546 // create join table
547 $joinTable = $mapping->joinTable;
548
549 $theJoinTable = $schema->createTable(
550 $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform),
551 );
552
553 foreach ($joinTable->options as $key => $val) {
554 $theJoinTable->addOption($key, $val);
555 }
556
557 $primaryKeyColumns = [];
558
559 // Build first FK constraint (relation table => source table)
560 $this->gatherRelationJoinColumns(
561 $joinTable->joinColumns,
562 $theJoinTable,
563 $class,
564 $mapping,
565 $primaryKeyColumns,
566 $addedFks,
567 $blacklistedFks,
568 );
569
570 // Build second FK constraint (relation table => target table)
571 $this->gatherRelationJoinColumns(
572 $joinTable->inverseJoinColumns,
573 $theJoinTable,
574 $foreignClass,
575 $mapping,
576 $primaryKeyColumns,
577 $addedFks,
578 $blacklistedFks,
579 );
580
581 $theJoinTable->setPrimaryKey($primaryKeyColumns);
582 }
583 }
584 }
585
586 /**
587 * Gets the class metadata that is responsible for the definition of the referenced column name.
588 *
589 * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
590 * not a simple field, go through all identifier field names that are associations recursively and
591 * find that referenced column name.
592 *
593 * TODO: Is there any way to make this code more pleasing?
594 *
595 * @psalm-return array{ClassMetadata, string}|null
596 */
597 private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null
598 {
599 $referencedFieldName = $class->getFieldName($referencedColumnName);
600
601 if ($class->hasField($referencedFieldName)) {
602 return [$class, $referencedFieldName];
603 }
604
605 if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) {
606 // it seems to be an entity as foreign key
607 foreach ($class->getIdentifierFieldNames() as $fieldName) {
608 if (
609 $class->hasAssociation($fieldName)
610 && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName
611 ) {
612 return $this->getDefiningClass(
613 $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity),
614 $class->getSingleAssociationReferencedJoinColumnName($fieldName),
615 );
616 }
617 }
618 }
619
620 return null;
621 }
622
623 /**
624 * Gathers columns and fk constraints that are required for one part of relationship.
625 *
626 * @psalm-param list<JoinColumnMapping> $joinColumns
627 * @psalm-param list<string> $primaryKeyColumns
628 * @psalm-param array<string, array{
629 * foreignTableName: string,
630 * foreignColumns: list<string>
631 * }> $addedFks
632 * @psalm-param array<string,bool> $blacklistedFks
633 *
634 * @throws MissingColumnException
635 */
636 private function gatherRelationJoinColumns(
637 array $joinColumns,
638 Table $theJoinTable,
639 ClassMetadata $class,
640 AssociationMapping $mapping,
641 array &$primaryKeyColumns,
642 array &$addedFks,
643 array &$blacklistedFks,
644 ): void {
645 $localColumns = [];
646 $foreignColumns = [];
647 $fkOptions = [];
648 $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform);
649 $uniqueConstraints = [];
650
651 foreach ($joinColumns as $joinColumn) {
652 [$definingClass, $referencedFieldName] = $this->getDefiningClass(
653 $class,
654 $joinColumn->referencedColumnName,
655 );
656
657 if (! $definingClass) {
658 throw MissingColumnException::fromColumnSourceAndTarget(
659 $joinColumn->referencedColumnName,
660 $mapping->sourceEntity,
661 $mapping->targetEntity,
662 );
663 }
664
665 $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
666 $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
667 $joinColumn,
668 $class,
669 $this->platform,
670 );
671
672 $primaryKeyColumns[] = $quotedColumnName;
673 $localColumns[] = $quotedColumnName;
674 $foreignColumns[] = $quotedRefColumnName;
675
676 if (! $theJoinTable->hasColumn($quotedColumnName)) {
677 // Only add the column to the table if it does not exist already.
678 // It might exist already if the foreign key is mapped into a regular
679 // property as well.
680
681 $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
682
683 $columnOptions = ['notnull' => false];
684
685 if (isset($joinColumn->columnDefinition)) {
686 $columnOptions['columnDefinition'] = $joinColumn->columnDefinition;
687 } elseif (isset($fieldMapping->columnDefinition)) {
688 $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition;
689 }
690
691 if (isset($joinColumn->nullable)) {
692 $columnOptions['notnull'] = ! $joinColumn->nullable;
693 }
694
695 $columnOptions += $this->gatherColumnOptions($fieldMapping);
696
697 if (isset($fieldMapping->length)) {
698 $columnOptions['length'] = $fieldMapping->length;
699 }
700
701 if ($fieldMapping->type === 'decimal') {
702 $columnOptions['scale'] = $fieldMapping->scale;
703 $columnOptions['precision'] = $fieldMapping->precision;
704 }
705
706 $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions;
707
708 $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions);
709 }
710
711 if (isset($joinColumn->unique) && $joinColumn->unique === true) {
712 $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
713 }
714
715 if (isset($joinColumn->onDelete)) {
716 $fkOptions['onDelete'] = $joinColumn->onDelete;
717 }
718 }
719
720 // Prefer unique constraints over implicit simple indexes created for foreign keys.
721 // Also avoids index duplication.
722 foreach ($uniqueConstraints as $indexName => $unique) {
723 $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
724 }
725
726 $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
727 if (
728 isset($addedFks[$compositeName])
729 && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
730 || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
731 ) {
732 foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
733 if (
734 count(array_diff($key->getLocalColumns(), $localColumns)) === 0
735 && (($key->getForeignTableName() !== $foreignTableName)
736 || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
737 ) {
738 $theJoinTable->removeForeignKey($fkName);
739 break;
740 }
741 }
742
743 $blacklistedFks[$compositeName] = true;
744 } elseif (! isset($blacklistedFks[$compositeName])) {
745 $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
746 $theJoinTable->addForeignKeyConstraint(
747 $foreignTableName,
748 $localColumns,
749 $foreignColumns,
750 $fkOptions,
751 );
752 }
753 }
754
755 /** @return mixed[] */
756 private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array
757 {
758 $mappingOptions = $mapping->options ?? [];
759
760 if (isset($mapping->enumType)) {
761 $mappingOptions['enumType'] = $mapping->enumType;
762 }
763
764 if (($mappingOptions['default'] ?? null) instanceof BackedEnum) {
765 $mappingOptions['default'] = $mappingOptions['default']->value;
766 }
767
768 if (empty($mappingOptions)) {
769 return [];
770 }
771
772 $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS));
773 $options['platformOptions'] = array_diff_key($mappingOptions, $options);
774
775 return $options;
776 }
777
778 /**
779 * Drops the database schema for the given classes.
780 *
781 * In any way when an exception is thrown it is suppressed since drop was
782 * issued for all classes of the schema and some probably just don't exist.
783 *
784 * @psalm-param list<ClassMetadata> $classes
785 */
786 public function dropSchema(array $classes): void
787 {
788 $dropSchemaSql = $this->getDropSchemaSQL($classes);
789 $conn = $this->em->getConnection();
790
791 foreach ($dropSchemaSql as $sql) {
792 try {
793 $conn->executeStatement($sql);
794 } catch (Throwable) {
795 // ignored
796 }
797 }
798 }
799
800 /**
801 * Drops all elements in the database of the current connection.
802 */
803 public function dropDatabase(): void
804 {
805 $dropSchemaSql = $this->getDropDatabaseSQL();
806 $conn = $this->em->getConnection();
807
808 foreach ($dropSchemaSql as $sql) {
809 $conn->executeStatement($sql);
810 }
811 }
812
813 /**
814 * Gets the SQL needed to drop the database schema for the connections database.
815 *
816 * @return list<string>
817 */
818 public function getDropDatabaseSQL(): array
819 {
820 return $this->schemaManager
821 ->introspectSchema()
822 ->toDropSql($this->platform);
823 }
824
825 /**
826 * Gets SQL to drop the tables defined by the passed classes.
827 *
828 * @psalm-param list<ClassMetadata> $classes
829 *
830 * @return list<string>
831 */
832 public function getDropSchemaSQL(array $classes): array
833 {
834 $schema = $this->getSchemaFromMetadata($classes);
835
836 $deployedSchema = $this->schemaManager->introspectSchema();
837
838 foreach ($schema->getTables() as $table) {
839 if (! $deployedSchema->hasTable($table->getName())) {
840 $schema->dropTable($table->getName());
841 }
842 }
843
844 if ($this->platform->supportsSequences()) {
845 foreach ($schema->getSequences() as $sequence) {
846 if (! $deployedSchema->hasSequence($sequence->getName())) {
847 $schema->dropSequence($sequence->getName());
848 }
849 }
850
851 foreach ($schema->getTables() as $table) {
852 $primaryKey = $table->getPrimaryKey();
853 if ($primaryKey === null) {
854 continue;
855 }
856
857 $columns = $primaryKey->getColumns();
858 if (count($columns) === 1) {
859 $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
860 if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) {
861 $schema->createSequence($checkSequence);
862 }
863 }
864 }
865 }
866
867 return $schema->toDropSql($this->platform);
868 }
869
870 /**
871 * Updates the database schema of the given classes by comparing the ClassMetadata
872 * instances to the current database schema that is inspected.
873 *
874 * @param mixed[] $classes
875 */
876 public function updateSchema(array $classes): void
877 {
878 $conn = $this->em->getConnection();
879
880 foreach ($this->getUpdateSchemaSql($classes) as $sql) {
881 $conn->executeStatement($sql);
882 }
883 }
884
885 /**
886 * Gets the sequence of SQL statements that need to be performed in order
887 * to bring the given class mappings in-synch with the relational schema.
888 *
889 * @param list<ClassMetadata> $classes The classes to consider.
890 *
891 * @return list<string> The sequence of SQL statements.
892 */
893 public function getUpdateSchemaSql(array $classes): array
894 {
895 $toSchema = $this->getSchemaFromMetadata($classes);
896 $fromSchema = $this->createSchemaForComparison($toSchema);
897 $comparator = $this->schemaManager->createComparator();
898 $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema);
899
900 return $this->platform->getAlterSchemaSQL($schemaDiff);
901 }
902
903 /**
904 * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison.
905 */
906 private function createSchemaForComparison(Schema $toSchema): Schema
907 {
908 $connection = $this->em->getConnection();
909
910 // backup schema assets filter
911 $config = $connection->getConfiguration();
912 $previousFilter = $config->getSchemaAssetsFilter();
913
914 if ($previousFilter === null) {
915 return $this->schemaManager->introspectSchema();
916 }
917
918 // whitelist assets we already know about in $toSchema, use the existing filter otherwise
919 $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool {
920 $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
921
922 return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
923 });
924
925 try {
926 return $this->schemaManager->introspectSchema();
927 } finally {
928 // restore schema assets filter
929 $config->setSchemaAssetsFilter($previousFilter);
930 }
931 }
932}
diff --git a/vendor/doctrine/orm/src/Tools/SchemaValidator.php b/vendor/doctrine/orm/src/Tools/SchemaValidator.php
new file mode 100644
index 0000000..fdfc003
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/SchemaValidator.php
@@ -0,0 +1,443 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use BackedEnum;
8use Doctrine\DBAL\Types\AsciiStringType;
9use Doctrine\DBAL\Types\BigIntType;
10use Doctrine\DBAL\Types\BooleanType;
11use Doctrine\DBAL\Types\DecimalType;
12use Doctrine\DBAL\Types\FloatType;
13use Doctrine\DBAL\Types\GuidType;
14use Doctrine\DBAL\Types\IntegerType;
15use Doctrine\DBAL\Types\JsonType;
16use Doctrine\DBAL\Types\SimpleArrayType;
17use Doctrine\DBAL\Types\SmallIntType;
18use Doctrine\DBAL\Types\StringType;
19use Doctrine\DBAL\Types\TextType;
20use Doctrine\DBAL\Types\Type;
21use Doctrine\ORM\EntityManagerInterface;
22use Doctrine\ORM\Mapping\ClassMetadata;
23use Doctrine\ORM\Mapping\FieldMapping;
24use ReflectionEnum;
25use ReflectionNamedType;
26
27use function array_diff;
28use function array_filter;
29use function array_key_exists;
30use function array_map;
31use function array_push;
32use function array_search;
33use function array_values;
34use function assert;
35use function class_exists;
36use function class_parents;
37use function count;
38use function implode;
39use function in_array;
40use function interface_exists;
41use function is_a;
42use function sprintf;
43
44/**
45 * Performs strict validation of the mapping schema
46 *
47 * @link www.doctrine-project.com
48 */
49class SchemaValidator
50{
51 /**
52 * It maps built-in Doctrine types to PHP types
53 */
54 private const BUILTIN_TYPES_MAP = [
55 AsciiStringType::class => ['string'],
56 BigIntType::class => ['int', 'string'],
57 BooleanType::class => ['bool'],
58 DecimalType::class => ['string'],
59 FloatType::class => ['float'],
60 GuidType::class => ['string'],
61 IntegerType::class => ['int'],
62 JsonType::class => ['array'],
63 SimpleArrayType::class => ['array'],
64 SmallIntType::class => ['int'],
65 StringType::class => ['string'],
66 TextType::class => ['string'],
67 ];
68
69 public function __construct(
70 private readonly EntityManagerInterface $em,
71 private readonly bool $validatePropertyTypes = true,
72 ) {
73 }
74
75 /**
76 * Checks the internal consistency of all mapping files.
77 *
78 * There are several checks that can't be done at runtime or are too expensive, which can be verified
79 * with this command. For example:
80 *
81 * 1. Check if a relation with "mappedBy" is actually connected to that specified field.
82 * 2. Check if "mappedBy" and "inversedBy" are consistent to each other.
83 * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns.
84 *
85 * @psalm-return array<string, list<string>>
86 */
87 public function validateMapping(): array
88 {
89 $errors = [];
90 $cmf = $this->em->getMetadataFactory();
91 $classes = $cmf->getAllMetadata();
92
93 foreach ($classes as $class) {
94 $ce = $this->validateClass($class);
95 if ($ce) {
96 $errors[$class->name] = $ce;
97 }
98 }
99
100 return $errors;
101 }
102
103 /**
104 * Validates a single class of the current.
105 *
106 * @return string[]
107 * @psalm-return list<string>
108 */
109 public function validateClass(ClassMetadata $class): array
110 {
111 $ce = [];
112 $cmf = $this->em->getMetadataFactory();
113
114 foreach ($class->fieldMappings as $fieldName => $mapping) {
115 if (! Type::hasType($mapping->type)) {
116 $ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping->type . "'.";
117 }
118 }
119
120 if ($this->validatePropertyTypes) {
121 array_push($ce, ...$this->validatePropertiesTypes($class));
122 }
123
124 foreach ($class->associationMappings as $fieldName => $assoc) {
125 if (! class_exists($assoc->targetEntity) || $cmf->isTransient($assoc->targetEntity)) {
126 $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.';
127
128 return $ce;
129 }
130
131 $targetMetadata = $cmf->getMetadataFor($assoc->targetEntity);
132
133 if ($targetMetadata->isMappedSuperclass) {
134 $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is a mapped superclass. This is not possible since there is no table that a foreign key could refer to.';
135
136 return $ce;
137 }
138
139 if (isset($assoc->id) && $targetMetadata->containsForeignIdentifier) {
140 $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' .
141 "the target entity '" . $targetMetadata->name . "' also maps an association as identifier.";
142 }
143
144 if (! $assoc->isOwningSide()) {
145 if ($targetMetadata->hasField($assoc->mappedBy)) {
146 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' .
147 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which is not defined as association, but as field.';
148 }
149
150 if (! $targetMetadata->hasAssociation($assoc->mappedBy)) {
151 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' .
152 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which does not exist.';
153 } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy === null) {
154 $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' .
155 'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
156 $assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' .
157 "'inversedBy=\"" . $fieldName . "\"' attribute.";
158 } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) {
159 $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
160 $assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' .
161 'inconsistent with each other.';
162 }
163 }
164
165 if ($assoc->isOwningSide() && $assoc->inversedBy !== null) {
166 if ($targetMetadata->hasField($assoc->inversedBy)) {
167 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' .
168 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.';
169 }
170
171 if (! $targetMetadata->hasAssociation($assoc->inversedBy)) {
172 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' .
173 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which does not exist.';
174 } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->isOwningSide()) {
175 $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' .
176 'bi-directional relationship, but the specified inversedBy association on the target-entity ' .
177 $assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' .
178 "'mappedBy=\"" . $fieldName . "\"' attribute.";
179 } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) {
180 $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' .
181 $assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' .
182 'inconsistent with each other.';
183 }
184
185 // Verify inverse side/owning side match each other
186 if (array_key_exists($assoc->inversedBy, $targetMetadata->associationMappings)) {
187 $targetAssoc = $targetMetadata->associationMappings[$assoc->inversedBy];
188 if ($assoc->isOneToOne() && ! $targetAssoc->isOneToOne()) {
189 $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' .
190 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-one as well.';
191 } elseif ($assoc->isManyToOne() && ! $targetAssoc->isOneToMany()) {
192 $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' .
193 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-many.';
194 } elseif ($assoc->isManyToMany() && ! $targetAssoc->isManyToMany()) {
195 $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' .
196 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be many-to-many as well.';
197 }
198 }
199 }
200
201 if ($assoc->isOwningSide()) {
202 if ($assoc->isManyToManyOwningSide()) {
203 $identifierColumns = $class->getIdentifierColumnNames();
204 foreach ($assoc->joinTable->joinColumns as $joinColumn) {
205 if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) {
206 $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " .
207 "has to be a primary key column on the target entity class '" . $class->name . "'.";
208 break;
209 }
210 }
211
212 $identifierColumns = $targetMetadata->getIdentifierColumnNames();
213 foreach ($assoc->joinTable->inverseJoinColumns as $inverseJoinColumn) {
214 if (! in_array($inverseJoinColumn->referencedColumnName, $identifierColumns, true)) {
215 $ce[] = "The referenced column name '" . $inverseJoinColumn->referencedColumnName . "' " .
216 "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'.";
217 break;
218 }
219 }
220
221 if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc->joinTable->inverseJoinColumns)) {
222 $ce[] = "The inverse join columns of the many-to-many table '" . $assoc->joinTable->name . "' " .
223 "have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " .
224 "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc->relationToTargetKeyColumns))) .
225 "' are missing.";
226 }
227
228 if (count($class->getIdentifierColumnNames()) !== count($assoc->joinTable->joinColumns)) {
229 $ce[] = "The join columns of the many-to-many table '" . $assoc->joinTable->name . "' " .
230 "have to contain to ALL identifier columns of the source entity '" . $class->name . "', " .
231 "however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc->relationToSourceKeyColumns))) .
232 "' are missing.";
233 }
234 } elseif ($assoc->isToOneOwningSide()) {
235 $identifierColumns = $targetMetadata->getIdentifierColumnNames();
236 foreach ($assoc->joinColumns as $joinColumn) {
237 if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) {
238 $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " .
239 "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'.";
240 }
241 }
242
243 if (count($identifierColumns) !== count($assoc->joinColumns)) {
244 $ids = [];
245
246 foreach ($assoc->joinColumns as $joinColumn) {
247 $ids[] = $joinColumn->name;
248 }
249
250 $ce[] = "The join columns of the association '" . $assoc->fieldName . "' " .
251 "have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " .
252 "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) .
253 "' are missing.";
254 }
255 }
256 }
257
258 if ($assoc->isOrdered()) {
259 foreach ($assoc->orderBy() as $orderField => $orientation) {
260 if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) {
261 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' .
262 $orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.';
263 continue;
264 }
265
266 if ($targetMetadata->isCollectionValuedAssociation($orderField)) {
267 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' .
268 $orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.';
269 continue;
270 }
271
272 if ($targetMetadata->isAssociationInverseSide($orderField)) {
273 $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' .
274 $orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.';
275 continue;
276 }
277 }
278 }
279 }
280
281 if (
282 ! $class->isInheritanceTypeNone()
283 && ! $class->isRootEntity()
284 && ($class->reflClass !== null && ! $class->reflClass->isAbstract())
285 && ! $class->isMappedSuperclass
286 && array_search($class->name, $class->discriminatorMap, true) === false
287 ) {
288 $ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " .
289 "not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " .
290 'All subclasses must be listed in the discriminator map.';
291 }
292
293 foreach ($class->subClasses as $subClass) {
294 if (! in_array($class->name, class_parents($subClass), true)) {
295 $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " .
296 "of '" . $class->name . "' but these entities are not related through inheritance.";
297 }
298 }
299
300 return $ce;
301 }
302
303 /**
304 * Checks if the Database Schema is in sync with the current metadata state.
305 */
306 public function schemaInSyncWithMetadata(): bool
307 {
308 return count($this->getUpdateSchemaList()) === 0;
309 }
310
311 /**
312 * Returns the list of missing Database Schema updates.
313 *
314 * @return array<string>
315 */
316 public function getUpdateSchemaList(): array
317 {
318 $schemaTool = new SchemaTool($this->em);
319
320 $allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
321
322 return $schemaTool->getUpdateSchemaSql($allMetadata);
323 }
324
325 /** @return list<string> containing the found issues */
326 private function validatePropertiesTypes(ClassMetadata $class): array
327 {
328 return array_values(
329 array_filter(
330 array_map(
331 function (FieldMapping $fieldMapping) use ($class): string|null {
332 $fieldName = $fieldMapping->fieldName;
333 assert(isset($class->reflFields[$fieldName]));
334 $propertyType = $class->reflFields[$fieldName]->getType();
335
336 // If the field type is not a built-in type, we cannot check it
337 if (! Type::hasType($fieldMapping->type)) {
338 return null;
339 }
340
341 // If the property type is not a named type, we cannot check it
342 if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') {
343 return null;
344 }
345
346 $metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type));
347
348 //If the metadata field type is not a mapped built-in type, we cannot check it
349 if ($metadataFieldType === null) {
350 return null;
351 }
352
353 $propertyType = $propertyType->getName();
354
355 // If the property type is the same as the metadata field type, we are ok
356 if (in_array($propertyType, $metadataFieldType, true)) {
357 return null;
358 }
359
360 if (is_a($propertyType, BackedEnum::class, true)) {
361 $backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
362
363 if (! in_array($backingType, $metadataFieldType, true)) {
364 return sprintf(
365 "The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
366 $class->name,
367 $fieldName,
368 $propertyType,
369 $backingType,
370 implode('|', $metadataFieldType),
371 );
372 }
373
374 if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) {
375 return null;
376 }
377
378 return sprintf(
379 "The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.",
380 $class->name,
381 $fieldName,
382 $propertyType,
383 $fieldMapping->enumType,
384 );
385 }
386
387 if (
388 isset($fieldMapping->enumType)
389 && $propertyType !== $fieldMapping->enumType
390 && interface_exists($propertyType)
391 && is_a($fieldMapping->enumType, $propertyType, true)
392 ) {
393 $backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType();
394
395 if (in_array($backingType, $metadataFieldType, true)) {
396 return null;
397 }
398
399 return sprintf(
400 "The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
401 $class->name,
402 $fieldName,
403 $fieldMapping->enumType,
404 $backingType,
405 implode('|', $metadataFieldType),
406 );
407 }
408
409 if (
410 $fieldMapping->type === 'json'
411 && in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
412 ) {
413 return null;
414 }
415
416 return sprintf(
417 "The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
418 $class->name,
419 $fieldName,
420 $propertyType,
421 implode('|', $metadataFieldType),
422 $fieldMapping->type,
423 );
424 },
425 $class->fieldMappings,
426 ),
427 ),
428 );
429 }
430
431 /**
432 * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
433 * customization around field types.
434 *
435 * @return list<string>|null
436 */
437 private function findBuiltInType(Type $type): array|null
438 {
439 $typeName = $type::class;
440
441 return self::BUILTIN_TYPES_MAP[$typeName] ?? null;
442 }
443}
diff --git a/vendor/doctrine/orm/src/Tools/ToolEvents.php b/vendor/doctrine/orm/src/Tools/ToolEvents.php
new file mode 100644
index 0000000..fac37fa
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/ToolEvents.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7class ToolEvents
8{
9 /**
10 * The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata()
11 * whenever an entity class is transformed into its table representation. It receives
12 * the current non-complete Schema instance, the Entity Metadata Class instance and
13 * the Schema Table instance of this entity.
14 */
15 public const postGenerateSchemaTable = 'postGenerateSchemaTable';
16
17 /**
18 * The postGenerateSchema event is triggered in SchemaTool#getSchemaFromMetadata()
19 * after all entity classes have been transformed into the related Schema structure.
20 * The EventArgs contain the EntityManager and the created Schema instance.
21 */
22 public const postGenerateSchema = 'postGenerateSchema';
23}
diff --git a/vendor/doctrine/orm/src/Tools/ToolsException.php b/vendor/doctrine/orm/src/Tools/ToolsException.php
new file mode 100644
index 0000000..e5cb973
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/ToolsException.php
@@ -0,0 +1,24 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools;
6
7use Doctrine\ORM\Exception\ORMException;
8use RuntimeException;
9use Throwable;
10
11/**
12 * Tools related Exceptions.
13 */
14class ToolsException extends RuntimeException implements ORMException
15{
16 public static function schemaToolFailure(string $sql, Throwable $e): self
17 {
18 return new self(
19 "Schema-Tool failed with Error '" . $e->getMessage() . "' while executing DDL: " . $sql,
20 0,
21 $e,
22 );
23 }
24}
diff --git a/vendor/doctrine/orm/src/TransactionRequiredException.php b/vendor/doctrine/orm/src/TransactionRequiredException.php
new file mode 100644
index 0000000..6114544
--- /dev/null
+++ b/vendor/doctrine/orm/src/TransactionRequiredException.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Exception\ORMException;
8use LogicException;
9
10/**
11 * Is thrown when a transaction is required for the current operation, but there is none open.
12 *
13 * @link www.doctrine-project.com
14 */
15class TransactionRequiredException extends LogicException implements ORMException
16{
17 public static function transactionRequired(): self
18 {
19 return new self('An open transaction is required for this operation.');
20 }
21}
diff --git a/vendor/doctrine/orm/src/UnexpectedResultException.php b/vendor/doctrine/orm/src/UnexpectedResultException.php
new file mode 100644
index 0000000..8d8c966
--- /dev/null
+++ b/vendor/doctrine/orm/src/UnexpectedResultException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use Doctrine\ORM\Exception\ORMException;
8use RuntimeException;
9
10/**
11 * Exception for a unexpected query result.
12 */
13class UnexpectedResultException extends RuntimeException implements ORMException
14{
15}
diff --git a/vendor/doctrine/orm/src/UnitOfWork.php b/vendor/doctrine/orm/src/UnitOfWork.php
new file mode 100644
index 0000000..26f17d2
--- /dev/null
+++ b/vendor/doctrine/orm/src/UnitOfWork.php
@@ -0,0 +1,3252 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM;
6
7use BackedEnum;
8use DateTimeInterface;
9use Doctrine\Common\Collections\ArrayCollection;
10use Doctrine\Common\Collections\Collection;
11use Doctrine\Common\EventManager;
12use Doctrine\DBAL;
13use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
14use Doctrine\DBAL\LockMode;
15use Doctrine\ORM\Cache\Persister\CachedPersister;
16use Doctrine\ORM\Event\ListenersInvoker;
17use Doctrine\ORM\Event\OnClearEventArgs;
18use Doctrine\ORM\Event\OnFlushEventArgs;
19use Doctrine\ORM\Event\PostFlushEventArgs;
20use Doctrine\ORM\Event\PostPersistEventArgs;
21use Doctrine\ORM\Event\PostRemoveEventArgs;
22use Doctrine\ORM\Event\PostUpdateEventArgs;
23use Doctrine\ORM\Event\PreFlushEventArgs;
24use Doctrine\ORM\Event\PrePersistEventArgs;
25use Doctrine\ORM\Event\PreRemoveEventArgs;
26use Doctrine\ORM\Event\PreUpdateEventArgs;
27use Doctrine\ORM\Exception\EntityIdentityCollisionException;
28use Doctrine\ORM\Exception\ORMException;
29use Doctrine\ORM\Exception\UnexpectedAssociationValue;
30use Doctrine\ORM\Id\AssignedGenerator;
31use Doctrine\ORM\Internal\HydrationCompleteHandler;
32use Doctrine\ORM\Internal\StronglyConnectedComponents;
33use Doctrine\ORM\Internal\TopologicalSort;
34use Doctrine\ORM\Mapping\AssociationMapping;
35use Doctrine\ORM\Mapping\ClassMetadata;
36use Doctrine\ORM\Mapping\MappingException;
37use Doctrine\ORM\Mapping\ToManyInverseSideMapping;
38use Doctrine\ORM\Persisters\Collection\CollectionPersister;
39use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
40use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
41use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
42use Doctrine\ORM\Persisters\Entity\EntityPersister;
43use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
44use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
45use Doctrine\ORM\Proxy\InternalProxy;
46use Doctrine\ORM\Utility\IdentifierFlattener;
47use Doctrine\Persistence\PropertyChangedListener;
48use Exception;
49use InvalidArgumentException;
50use RuntimeException;
51use Stringable;
52use Throwable;
53use UnexpectedValueException;
54
55use function array_chunk;
56use function array_combine;
57use function array_diff_key;
58use function array_filter;
59use function array_key_exists;
60use function array_map;
61use function array_sum;
62use function array_values;
63use function assert;
64use function current;
65use function get_debug_type;
66use function implode;
67use function in_array;
68use function is_array;
69use function is_object;
70use function reset;
71use function spl_object_id;
72use function sprintf;
73use function strtolower;
74
75/**
76 * The UnitOfWork is responsible for tracking changes to objects during an
77 * "object-level" transaction and for writing out changes to the database
78 * in the correct order.
79 *
80 * Internal note: This class contains highly performance-sensitive code.
81 */
82class UnitOfWork implements PropertyChangedListener
83{
84 /**
85 * An entity is in MANAGED state when its persistence is managed by an EntityManager.
86 */
87 public const STATE_MANAGED = 1;
88
89 /**
90 * An entity is new if it has just been instantiated (i.e. using the "new" operator)
91 * and is not (yet) managed by an EntityManager.
92 */
93 public const STATE_NEW = 2;
94
95 /**
96 * A detached entity is an instance with persistent state and identity that is not
97 * (or no longer) associated with an EntityManager (and a UnitOfWork).
98 */
99 public const STATE_DETACHED = 3;
100
101 /**
102 * A removed entity instance is an instance with a persistent identity,
103 * associated with an EntityManager, whose persistent state will be deleted
104 * on commit.
105 */
106 public const STATE_REMOVED = 4;
107
108 /**
109 * Hint used to collect all primary keys of associated entities during hydration
110 * and execute it in a dedicated query afterwards
111 *
112 * @see https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
113 */
114 public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
115
116 /**
117 * The identity map that holds references to all managed entities that have
118 * an identity. The entities are grouped by their class name.
119 * Since all classes in a hierarchy must share the same identifier set,
120 * we always take the root class name of the hierarchy.
121 *
122 * @psalm-var array<class-string, array<string, object>>
123 */
124 private array $identityMap = [];
125
126 /**
127 * Map of all identifiers of managed entities.
128 * Keys are object ids (spl_object_id).
129 *
130 * @psalm-var array<int, array<string, mixed>>
131 */
132 private array $entityIdentifiers = [];
133
134 /**
135 * Map of the original entity data of managed entities.
136 * Keys are object ids (spl_object_id). This is used for calculating changesets
137 * at commit time.
138 *
139 * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
140 * A value will only really be copied if the value in the entity is modified
141 * by the user.
142 *
143 * @psalm-var array<int, array<string, mixed>>
144 */
145 private array $originalEntityData = [];
146
147 /**
148 * Map of entity changes. Keys are object ids (spl_object_id).
149 * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
150 *
151 * @psalm-var array<int, array<string, array{mixed, mixed}>>
152 */
153 private array $entityChangeSets = [];
154
155 /**
156 * The (cached) states of any known entities.
157 * Keys are object ids (spl_object_id).
158 *
159 * @psalm-var array<int, self::STATE_*>
160 */
161 private array $entityStates = [];
162
163 /**
164 * Map of entities that are scheduled for dirty checking at commit time.
165 * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
166 * Keys are object ids (spl_object_id).
167 *
168 * @psalm-var array<class-string, array<int, mixed>>
169 */
170 private array $scheduledForSynchronization = [];
171
172 /**
173 * A list of all pending entity insertions.
174 *
175 * @psalm-var array<int, object>
176 */
177 private array $entityInsertions = [];
178
179 /**
180 * A list of all pending entity updates.
181 *
182 * @psalm-var array<int, object>
183 */
184 private array $entityUpdates = [];
185
186 /**
187 * Any pending extra updates that have been scheduled by persisters.
188 *
189 * @psalm-var array<int, array{object, array<string, array{mixed, mixed}>}>
190 */
191 private array $extraUpdates = [];
192
193 /**
194 * A list of all pending entity deletions.
195 *
196 * @psalm-var array<int, object>
197 */
198 private array $entityDeletions = [];
199
200 /**
201 * New entities that were discovered through relationships that were not
202 * marked as cascade-persist. During flush, this array is populated and
203 * then pruned of any entities that were discovered through a valid
204 * cascade-persist path. (Leftovers cause an error.)
205 *
206 * Keys are OIDs, payload is a two-item array describing the association
207 * and the entity.
208 *
209 * @var array<int, array{AssociationMapping, object}> indexed by respective object spl_object_id()
210 */
211 private array $nonCascadedNewDetectedEntities = [];
212
213 /**
214 * All pending collection deletions.
215 *
216 * @psalm-var array<int, PersistentCollection<array-key, object>>
217 */
218 private array $collectionDeletions = [];
219
220 /**
221 * All pending collection updates.
222 *
223 * @psalm-var array<int, PersistentCollection<array-key, object>>
224 */
225 private array $collectionUpdates = [];
226
227 /**
228 * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
229 * At the end of the UnitOfWork all these collections will make new snapshots
230 * of their data.
231 *
232 * @psalm-var array<int, PersistentCollection<array-key, object>>
233 */
234 private array $visitedCollections = [];
235
236 /**
237 * List of collections visited during the changeset calculation that contain to-be-removed
238 * entities and need to have keys removed post commit.
239 *
240 * Indexed by Collection object ID, which also serves as the key in self::$visitedCollections;
241 * values are the key names that need to be removed.
242 *
243 * @psalm-var array<int, array<array-key, true>>
244 */
245 private array $pendingCollectionElementRemovals = [];
246
247 /**
248 * The entity persister instances used to persist entity instances.
249 *
250 * @psalm-var array<string, EntityPersister>
251 */
252 private array $persisters = [];
253
254 /**
255 * The collection persister instances used to persist collections.
256 *
257 * @psalm-var array<array-key, CollectionPersister>
258 */
259 private array $collectionPersisters = [];
260
261 /**
262 * The EventManager used for dispatching events.
263 */
264 private readonly EventManager $evm;
265
266 /**
267 * The ListenersInvoker used for dispatching events.
268 */
269 private readonly ListenersInvoker $listenersInvoker;
270
271 /**
272 * The IdentifierFlattener used for manipulating identifiers
273 */
274 private readonly IdentifierFlattener $identifierFlattener;
275
276 /**
277 * Orphaned entities that are scheduled for removal.
278 *
279 * @psalm-var array<int, object>
280 */
281 private array $orphanRemovals = [];
282
283 /**
284 * Read-Only objects are never evaluated
285 *
286 * @var array<int, true>
287 */
288 private array $readOnlyObjects = [];
289
290 /**
291 * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
292 *
293 * @psalm-var array<class-string, array<string, mixed>>
294 */
295 private array $eagerLoadingEntities = [];
296
297 /** @var array<string, array<string, mixed>> */
298 private array $eagerLoadingCollections = [];
299
300 protected bool $hasCache = false;
301
302 /**
303 * Helper for handling completion of hydration
304 */
305 private readonly HydrationCompleteHandler $hydrationCompleteHandler;
306
307 /**
308 * Initializes a new UnitOfWork instance, bound to the given EntityManager.
309 *
310 * @param EntityManagerInterface $em The EntityManager that "owns" this UnitOfWork instance.
311 */
312 public function __construct(
313 private readonly EntityManagerInterface $em,
314 ) {
315 $this->evm = $em->getEventManager();
316 $this->listenersInvoker = new ListenersInvoker($em);
317 $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled();
318 $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory());
319 $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
320 }
321
322 /**
323 * Commits the UnitOfWork, executing all operations that have been postponed
324 * up to this point. The state of all managed entities will be synchronized with
325 * the database.
326 *
327 * The operations are executed in the following order:
328 *
329 * 1) All entity insertions
330 * 2) All entity updates
331 * 3) All collection deletions
332 * 4) All collection updates
333 * 5) All entity deletions
334 *
335 * @throws Exception
336 */
337 public function commit(): void
338 {
339 $connection = $this->em->getConnection();
340
341 if ($connection instanceof PrimaryReadReplicaConnection) {
342 $connection->ensureConnectedToPrimary();
343 }
344
345 // Raise preFlush
346 if ($this->evm->hasListeners(Events::preFlush)) {
347 $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
348 }
349
350 // Compute changes done since last commit.
351 $this->computeChangeSets();
352
353 if (
354 ! ($this->entityInsertions ||
355 $this->entityDeletions ||
356 $this->entityUpdates ||
357 $this->collectionUpdates ||
358 $this->collectionDeletions ||
359 $this->orphanRemovals)
360 ) {
361 $this->dispatchOnFlushEvent();
362 $this->dispatchPostFlushEvent();
363
364 $this->postCommitCleanup();
365
366 return; // Nothing to do.
367 }
368
369 $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
370
371 if ($this->orphanRemovals) {
372 foreach ($this->orphanRemovals as $orphan) {
373 $this->remove($orphan);
374 }
375 }
376
377 $this->dispatchOnFlushEvent();
378
379 $conn = $this->em->getConnection();
380 $conn->beginTransaction();
381
382 try {
383 // Collection deletions (deletions of complete collections)
384 foreach ($this->collectionDeletions as $collectionToDelete) {
385 // Deferred explicit tracked collections can be removed only when owning relation was persisted
386 $owner = $collectionToDelete->getOwner();
387
388 if ($this->em->getClassMetadata($owner::class)->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
389 $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
390 }
391 }
392
393 if ($this->entityInsertions) {
394 // Perform entity insertions first, so that all new entities have their rows in the database
395 // and can be referred to by foreign keys. The commit order only needs to take new entities
396 // into account (new entities referring to other new entities), since all other types (entities
397 // with updates or scheduled deletions) are currently not a problem, since they are already
398 // in the database.
399 $this->executeInserts();
400 }
401
402 if ($this->entityUpdates) {
403 // Updates do not need to follow a particular order
404 $this->executeUpdates();
405 }
406
407 // Extra updates that were requested by persisters.
408 // This may include foreign keys that could not be set when an entity was inserted,
409 // which may happen in the case of circular foreign key relationships.
410 if ($this->extraUpdates) {
411 $this->executeExtraUpdates();
412 }
413
414 // Collection updates (deleteRows, updateRows, insertRows)
415 // No particular order is necessary, since all entities themselves are already
416 // in the database
417 foreach ($this->collectionUpdates as $collectionToUpdate) {
418 $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
419 }
420
421 // Entity deletions come last. Their order only needs to take care of other deletions
422 // (first delete entities depending upon others, before deleting depended-upon entities).
423 if ($this->entityDeletions) {
424 $this->executeDeletions();
425 }
426
427 $commitFailed = false;
428 try {
429 if ($conn->commit() === false) {
430 $commitFailed = true;
431 }
432 } catch (DBAL\Exception $e) {
433 $commitFailed = true;
434 }
435
436 if ($commitFailed) {
437 throw new OptimisticLockException('Commit failed', null, $e ?? null);
438 }
439 } catch (Throwable $e) {
440 $this->em->close();
441
442 if ($conn->isTransactionActive()) {
443 $conn->rollBack();
444 }
445
446 $this->afterTransactionRolledBack();
447
448 throw $e;
449 }
450
451 $this->afterTransactionComplete();
452
453 // Unset removed entities from collections, and take new snapshots from
454 // all visited collections.
455 foreach ($this->visitedCollections as $coid => $coll) {
456 if (isset($this->pendingCollectionElementRemovals[$coid])) {
457 foreach ($this->pendingCollectionElementRemovals[$coid] as $key => $valueIgnored) {
458 unset($coll[$key]);
459 }
460 }
461
462 $coll->takeSnapshot();
463 }
464
465 $this->dispatchPostFlushEvent();
466
467 $this->postCommitCleanup();
468 }
469
470 private function postCommitCleanup(): void
471 {
472 $this->entityInsertions =
473 $this->entityUpdates =
474 $this->entityDeletions =
475 $this->extraUpdates =
476 $this->collectionUpdates =
477 $this->nonCascadedNewDetectedEntities =
478 $this->collectionDeletions =
479 $this->pendingCollectionElementRemovals =
480 $this->visitedCollections =
481 $this->orphanRemovals =
482 $this->entityChangeSets =
483 $this->scheduledForSynchronization = [];
484 }
485
486 /**
487 * Computes the changesets of all entities scheduled for insertion.
488 */
489 private function computeScheduleInsertsChangeSets(): void
490 {
491 foreach ($this->entityInsertions as $entity) {
492 $class = $this->em->getClassMetadata($entity::class);
493
494 $this->computeChangeSet($class, $entity);
495 }
496 }
497
498 /**
499 * Executes any extra updates that have been scheduled.
500 */
501 private function executeExtraUpdates(): void
502 {
503 foreach ($this->extraUpdates as $oid => $update) {
504 [$entity, $changeset] = $update;
505
506 $this->entityChangeSets[$oid] = $changeset;
507 $this->getEntityPersister($entity::class)->update($entity);
508 }
509
510 $this->extraUpdates = [];
511 }
512
513 /**
514 * Gets the changeset for an entity.
515 *
516 * @return mixed[][]
517 * @psalm-return array<string, array{mixed, mixed}|PersistentCollection>
518 */
519 public function & getEntityChangeSet(object $entity): array
520 {
521 $oid = spl_object_id($entity);
522 $data = [];
523
524 if (! isset($this->entityChangeSets[$oid])) {
525 return $data;
526 }
527
528 return $this->entityChangeSets[$oid];
529 }
530
531 /**
532 * Computes the changes that happened to a single entity.
533 *
534 * Modifies/populates the following properties:
535 *
536 * {@link _originalEntityData}
537 * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
538 * then it was not fetched from the database and therefore we have no original
539 * entity data yet. All of the current entity data is stored as the original entity data.
540 *
541 * {@link _entityChangeSets}
542 * The changes detected on all properties of the entity are stored there.
543 * A change is a tuple array where the first entry is the old value and the second
544 * entry is the new value of the property. Changesets are used by persisters
545 * to INSERT/UPDATE the persistent entity state.
546 *
547 * {@link _entityUpdates}
548 * If the entity is already fully MANAGED (has been fetched from the database before)
549 * and any changes to its properties are detected, then a reference to the entity is stored
550 * there to mark it for an update.
551 *
552 * {@link _collectionDeletions}
553 * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
554 * then this collection is marked for deletion.
555 *
556 * @param ClassMetadata $class The class descriptor of the entity.
557 * @param object $entity The entity for which to compute the changes.
558 * @psalm-param ClassMetadata<T> $class
559 * @psalm-param T $entity
560 *
561 * @template T of object
562 *
563 * @ignore
564 */
565 public function computeChangeSet(ClassMetadata $class, object $entity): void
566 {
567 $oid = spl_object_id($entity);
568
569 if (isset($this->readOnlyObjects[$oid])) {
570 return;
571 }
572
573 if (! $class->isInheritanceTypeNone()) {
574 $class = $this->em->getClassMetadata($entity::class);
575 }
576
577 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
578
579 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
580 $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
581 }
582
583 $actualData = [];
584
585 foreach ($class->reflFields as $name => $refProp) {
586 $value = $refProp->getValue($entity);
587
588 if ($class->isCollectionValuedAssociation($name) && $value !== null) {
589 if ($value instanceof PersistentCollection) {
590 if ($value->getOwner() === $entity) {
591 $actualData[$name] = $value;
592 continue;
593 }
594
595 $value = new ArrayCollection($value->getValues());
596 }
597
598 // If $value is not a Collection then use an ArrayCollection.
599 if (! $value instanceof Collection) {
600 $value = new ArrayCollection($value);
601 }
602
603 $assoc = $class->associationMappings[$name];
604 assert($assoc->isToMany());
605
606 // Inject PersistentCollection
607 $value = new PersistentCollection(
608 $this->em,
609 $this->em->getClassMetadata($assoc->targetEntity),
610 $value,
611 );
612 $value->setOwner($entity, $assoc);
613 $value->setDirty(! $value->isEmpty());
614
615 $refProp->setValue($entity, $value);
616
617 $actualData[$name] = $value;
618
619 continue;
620 }
621
622 if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
623 $actualData[$name] = $value;
624 }
625 }
626
627 if (! isset($this->originalEntityData[$oid])) {
628 // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
629 // These result in an INSERT.
630 $this->originalEntityData[$oid] = $actualData;
631 $changeSet = [];
632
633 foreach ($actualData as $propName => $actualValue) {
634 if (! isset($class->associationMappings[$propName])) {
635 $changeSet[$propName] = [null, $actualValue];
636
637 continue;
638 }
639
640 $assoc = $class->associationMappings[$propName];
641
642 if ($assoc->isToOneOwningSide()) {
643 $changeSet[$propName] = [null, $actualValue];
644 }
645 }
646
647 $this->entityChangeSets[$oid] = $changeSet;
648 } else {
649 // Entity is "fully" MANAGED: it was already fully persisted before
650 // and we have a copy of the original data
651 $originalData = $this->originalEntityData[$oid];
652 $changeSet = [];
653
654 foreach ($actualData as $propName => $actualValue) {
655 // skip field, its a partially omitted one!
656 if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
657 continue;
658 }
659
660 $orgValue = $originalData[$propName];
661
662 if (! empty($class->fieldMappings[$propName]->enumType)) {
663 if (is_array($orgValue)) {
664 foreach ($orgValue as $id => $val) {
665 if ($val instanceof BackedEnum) {
666 $orgValue[$id] = $val->value;
667 }
668 }
669 } else {
670 if ($orgValue instanceof BackedEnum) {
671 $orgValue = $orgValue->value;
672 }
673 }
674 }
675
676 // skip if value haven't changed
677 if ($orgValue === $actualValue) {
678 continue;
679 }
680
681 // if regular field
682 if (! isset($class->associationMappings[$propName])) {
683 $changeSet[$propName] = [$orgValue, $actualValue];
684
685 continue;
686 }
687
688 $assoc = $class->associationMappings[$propName];
689
690 // Persistent collection was exchanged with the "originally"
691 // created one. This can only mean it was cloned and replaced
692 // on another entity.
693 if ($actualValue instanceof PersistentCollection) {
694 assert($assoc->isToMany());
695 $owner = $actualValue->getOwner();
696 if ($owner === null) { // cloned
697 $actualValue->setOwner($entity, $assoc);
698 } elseif ($owner !== $entity) { // no clone, we have to fix
699 if (! $actualValue->isInitialized()) {
700 $actualValue->initialize(); // we have to do this otherwise the cols share state
701 }
702
703 $newValue = clone $actualValue;
704 $newValue->setOwner($entity, $assoc);
705 $class->reflFields[$propName]->setValue($entity, $newValue);
706 }
707 }
708
709 if ($orgValue instanceof PersistentCollection) {
710 // A PersistentCollection was de-referenced, so delete it.
711 $coid = spl_object_id($orgValue);
712
713 if (isset($this->collectionDeletions[$coid])) {
714 continue;
715 }
716
717 $this->collectionDeletions[$coid] = $orgValue;
718 $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
719
720 continue;
721 }
722
723 if ($assoc->isToOne()) {
724 if ($assoc->isOwningSide()) {
725 $changeSet[$propName] = [$orgValue, $actualValue];
726 }
727
728 if ($orgValue !== null && $assoc->orphanRemoval) {
729 assert(is_object($orgValue));
730 $this->scheduleOrphanRemoval($orgValue);
731 }
732 }
733 }
734
735 if ($changeSet) {
736 $this->entityChangeSets[$oid] = $changeSet;
737 $this->originalEntityData[$oid] = $actualData;
738 $this->entityUpdates[$oid] = $entity;
739 }
740 }
741
742 // Look for changes in associations of the entity
743 foreach ($class->associationMappings as $field => $assoc) {
744 $val = $class->reflFields[$field]->getValue($entity);
745 if ($val === null) {
746 continue;
747 }
748
749 $this->computeAssociationChanges($assoc, $val);
750
751 if (
752 ! isset($this->entityChangeSets[$oid]) &&
753 $assoc->isManyToManyOwningSide() &&
754 $val instanceof PersistentCollection &&
755 $val->isDirty()
756 ) {
757 $this->entityChangeSets[$oid] = [];
758 $this->originalEntityData[$oid] = $actualData;
759 $this->entityUpdates[$oid] = $entity;
760 }
761 }
762 }
763
764 /**
765 * Computes all the changes that have been done to entities and collections
766 * since the last commit and stores these changes in the _entityChangeSet map
767 * temporarily for access by the persisters, until the UoW commit is finished.
768 */
769 public function computeChangeSets(): void
770 {
771 // Compute changes for INSERTed entities first. This must always happen.
772 $this->computeScheduleInsertsChangeSets();
773
774 // Compute changes for other MANAGED entities. Change tracking policies take effect here.
775 foreach ($this->identityMap as $className => $entities) {
776 $class = $this->em->getClassMetadata($className);
777
778 // Skip class if instances are read-only
779 if ($class->isReadOnly) {
780 continue;
781 }
782
783 $entitiesToProcess = match (true) {
784 $class->isChangeTrackingDeferredImplicit() => $entities,
785 isset($this->scheduledForSynchronization[$className]) => $this->scheduledForSynchronization[$className],
786 default => [],
787 };
788
789 foreach ($entitiesToProcess as $entity) {
790 // Ignore uninitialized proxy objects
791 if ($this->isUninitializedObject($entity)) {
792 continue;
793 }
794
795 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
796 $oid = spl_object_id($entity);
797
798 if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
799 $this->computeChangeSet($class, $entity);
800 }
801 }
802 }
803 }
804
805 /**
806 * Computes the changes of an association.
807 *
808 * @param mixed $value The value of the association.
809 *
810 * @throws ORMInvalidArgumentException
811 * @throws ORMException
812 */
813 private function computeAssociationChanges(AssociationMapping $assoc, mixed $value): void
814 {
815 if ($this->isUninitializedObject($value)) {
816 return;
817 }
818
819 // If this collection is dirty, schedule it for updates
820 if ($value instanceof PersistentCollection && $value->isDirty()) {
821 $coid = spl_object_id($value);
822
823 $this->collectionUpdates[$coid] = $value;
824 $this->visitedCollections[$coid] = $value;
825 }
826
827 // Look through the entities, and in any of their associations,
828 // for transient (new) entities, recursively. ("Persistence by reachability")
829 // Unwrap. Uninitialized collections will simply be empty.
830 $unwrappedValue = $assoc->isToOne() ? [$value] : $value->unwrap();
831 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
832
833 foreach ($unwrappedValue as $key => $entry) {
834 if (! ($entry instanceof $targetClass->name)) {
835 throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
836 }
837
838 $state = $this->getEntityState($entry, self::STATE_NEW);
839
840 if (! ($entry instanceof $assoc->targetEntity)) {
841 throw UnexpectedAssociationValue::create(
842 $assoc->sourceEntity,
843 $assoc->fieldName,
844 get_debug_type($entry),
845 $assoc->targetEntity,
846 );
847 }
848
849 switch ($state) {
850 case self::STATE_NEW:
851 if (! $assoc->isCascadePersist()) {
852 /*
853 * For now just record the details, because this may
854 * not be an issue if we later discover another pathway
855 * through the object-graph where cascade-persistence
856 * is enabled for this object.
857 */
858 $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$assoc, $entry];
859
860 break;
861 }
862
863 $this->persistNew($targetClass, $entry);
864 $this->computeChangeSet($targetClass, $entry);
865
866 break;
867
868 case self::STATE_REMOVED:
869 // Consume the $value as array (it's either an array or an ArrayAccess)
870 // and remove the element from Collection.
871 if (! $assoc->isToMany()) {
872 break;
873 }
874
875 $coid = spl_object_id($value);
876 $this->visitedCollections[$coid] = $value;
877
878 if (! isset($this->pendingCollectionElementRemovals[$coid])) {
879 $this->pendingCollectionElementRemovals[$coid] = [];
880 }
881
882 $this->pendingCollectionElementRemovals[$coid][$key] = true;
883 break;
884
885 case self::STATE_DETACHED:
886 // Can actually not happen right now as we assume STATE_NEW,
887 // so the exception will be raised from the DBAL layer (constraint violation).
888 throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
889
890 default:
891 // MANAGED associated entities are already taken into account
892 // during changeset calculation anyway, since they are in the identity map.
893 }
894 }
895 }
896
897 /**
898 * @psalm-param ClassMetadata<T> $class
899 * @psalm-param T $entity
900 *
901 * @template T of object
902 */
903 private function persistNew(ClassMetadata $class, object $entity): void
904 {
905 $oid = spl_object_id($entity);
906 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
907
908 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
909 $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new PrePersistEventArgs($entity, $this->em), $invoke);
910 }
911
912 $idGen = $class->idGenerator;
913
914 if (! $idGen->isPostInsertGenerator()) {
915 $idValue = $idGen->generateId($this->em, $entity);
916
917 if (! $idGen instanceof AssignedGenerator) {
918 $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
919
920 $class->setIdentifierValues($entity, $idValue);
921 }
922
923 // Some identifiers may be foreign keys to new entities.
924 // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
925 if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
926 $this->entityIdentifiers[$oid] = $idValue;
927 }
928 }
929
930 $this->entityStates[$oid] = self::STATE_MANAGED;
931
932 $this->scheduleForInsert($entity);
933 }
934
935 /** @param mixed[] $idValue */
936 private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue): bool
937 {
938 foreach ($idValue as $idField => $idFieldValue) {
939 if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
940 return true;
941 }
942 }
943
944 return false;
945 }
946
947 /**
948 * INTERNAL:
949 * Computes the changeset of an individual entity, independently of the
950 * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
951 *
952 * The passed entity must be a managed entity. If the entity already has a change set
953 * because this method is invoked during a commit cycle then the change sets are added.
954 * whereby changes detected in this method prevail.
955 *
956 * @param ClassMetadata $class The class descriptor of the entity.
957 * @param object $entity The entity for which to (re)calculate the change set.
958 * @psalm-param ClassMetadata<T> $class
959 * @psalm-param T $entity
960 *
961 * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
962 *
963 * @template T of object
964 * @ignore
965 */
966 public function recomputeSingleEntityChangeSet(ClassMetadata $class, object $entity): void
967 {
968 $oid = spl_object_id($entity);
969
970 if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
971 throw ORMInvalidArgumentException::entityNotManaged($entity);
972 }
973
974 if (! $class->isInheritanceTypeNone()) {
975 $class = $this->em->getClassMetadata($entity::class);
976 }
977
978 $actualData = [];
979
980 foreach ($class->reflFields as $name => $refProp) {
981 if (
982 ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
983 && ($name !== $class->versionField)
984 && ! $class->isCollectionValuedAssociation($name)
985 ) {
986 $actualData[$name] = $refProp->getValue($entity);
987 }
988 }
989
990 if (! isset($this->originalEntityData[$oid])) {
991 throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
992 }
993
994 $originalData = $this->originalEntityData[$oid];
995 $changeSet = [];
996
997 foreach ($actualData as $propName => $actualValue) {
998 $orgValue = $originalData[$propName] ?? null;
999
1000 if (isset($class->fieldMappings[$propName]->enumType)) {
1001 if (is_array($orgValue)) {
1002 foreach ($orgValue as $id => $val) {
1003 if ($val instanceof BackedEnum) {
1004 $orgValue[$id] = $val->value;
1005 }
1006 }
1007 } else {
1008 if ($orgValue instanceof BackedEnum) {
1009 $orgValue = $orgValue->value;
1010 }
1011 }
1012 }
1013
1014 if ($orgValue !== $actualValue) {
1015 $changeSet[$propName] = [$orgValue, $actualValue];
1016 }
1017 }
1018
1019 if ($changeSet) {
1020 if (isset($this->entityChangeSets[$oid])) {
1021 $this->entityChangeSets[$oid] = [...$this->entityChangeSets[$oid], ...$changeSet];
1022 } elseif (! isset($this->entityInsertions[$oid])) {
1023 $this->entityChangeSets[$oid] = $changeSet;
1024 $this->entityUpdates[$oid] = $entity;
1025 }
1026
1027 $this->originalEntityData[$oid] = $actualData;
1028 }
1029 }
1030
1031 /**
1032 * Executes entity insertions
1033 */
1034 private function executeInserts(): void
1035 {
1036 $entities = $this->computeInsertExecutionOrder();
1037 $eventsToDispatch = [];
1038
1039 foreach ($entities as $entity) {
1040 $oid = spl_object_id($entity);
1041 $class = $this->em->getClassMetadata($entity::class);
1042 $persister = $this->getEntityPersister($class->name);
1043
1044 $persister->addInsert($entity);
1045
1046 unset($this->entityInsertions[$oid]);
1047
1048 $persister->executeInserts();
1049
1050 if (! isset($this->entityIdentifiers[$oid])) {
1051 //entity was not added to identity map because some identifiers are foreign keys to new entities.
1052 //add it now
1053 $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1054 }
1055
1056 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1057
1058 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1059 $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
1060 }
1061 }
1062
1063 // Defer dispatching `postPersist` events to until all entities have been inserted and post-insert
1064 // IDs have been assigned.
1065 foreach ($eventsToDispatch as $event) {
1066 $this->listenersInvoker->invoke(
1067 $event['class'],
1068 Events::postPersist,
1069 $event['entity'],
1070 new PostPersistEventArgs($event['entity'], $this->em),
1071 $event['invoke'],
1072 );
1073 }
1074 }
1075
1076 /**
1077 * @psalm-param ClassMetadata<T> $class
1078 * @psalm-param T $entity
1079 *
1080 * @template T of object
1081 */
1082 private function addToEntityIdentifiersAndEntityMap(
1083 ClassMetadata $class,
1084 int $oid,
1085 object $entity,
1086 ): void {
1087 $identifier = [];
1088
1089 foreach ($class->getIdentifierFieldNames() as $idField) {
1090 $origValue = $class->getFieldValue($entity, $idField);
1091
1092 $value = null;
1093 if (isset($class->associationMappings[$idField])) {
1094 // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1095 $value = $this->getSingleIdentifierValue($origValue);
1096 }
1097
1098 $identifier[$idField] = $value ?? $origValue;
1099 $this->originalEntityData[$oid][$idField] = $origValue;
1100 }
1101
1102 $this->entityStates[$oid] = self::STATE_MANAGED;
1103 $this->entityIdentifiers[$oid] = $identifier;
1104
1105 $this->addToIdentityMap($entity);
1106 }
1107
1108 /**
1109 * Executes all entity updates
1110 */
1111 private function executeUpdates(): void
1112 {
1113 foreach ($this->entityUpdates as $oid => $entity) {
1114 $class = $this->em->getClassMetadata($entity::class);
1115 $persister = $this->getEntityPersister($class->name);
1116 $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1117 $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1118
1119 if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1120 $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1121
1122 $this->recomputeSingleEntityChangeSet($class, $entity);
1123 }
1124
1125 if (! empty($this->entityChangeSets[$oid])) {
1126 $persister->update($entity);
1127 }
1128
1129 unset($this->entityUpdates[$oid]);
1130
1131 if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1132 $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new PostUpdateEventArgs($entity, $this->em), $postUpdateInvoke);
1133 }
1134 }
1135 }
1136
1137 /**
1138 * Executes all entity deletions
1139 */
1140 private function executeDeletions(): void
1141 {
1142 $entities = $this->computeDeleteExecutionOrder();
1143 $eventsToDispatch = [];
1144
1145 foreach ($entities as $entity) {
1146 $this->removeFromIdentityMap($entity);
1147
1148 $oid = spl_object_id($entity);
1149 $class = $this->em->getClassMetadata($entity::class);
1150 $persister = $this->getEntityPersister($class->name);
1151 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1152
1153 $persister->delete($entity);
1154
1155 unset(
1156 $this->entityDeletions[$oid],
1157 $this->entityIdentifiers[$oid],
1158 $this->originalEntityData[$oid],
1159 $this->entityStates[$oid],
1160 );
1161
1162 // Entity with this $oid after deletion treated as NEW, even if the $oid
1163 // is obtained by a new entity because the old one went out of scope.
1164 //$this->entityStates[$oid] = self::STATE_NEW;
1165 if (! $class->isIdentifierNatural()) {
1166 $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1167 }
1168
1169 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1170 $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
1171 }
1172 }
1173
1174 // Defer dispatching `postRemove` events to until all entities have been removed.
1175 foreach ($eventsToDispatch as $event) {
1176 $this->listenersInvoker->invoke(
1177 $event['class'],
1178 Events::postRemove,
1179 $event['entity'],
1180 new PostRemoveEventArgs($event['entity'], $this->em),
1181 $event['invoke'],
1182 );
1183 }
1184 }
1185
1186 /** @return list<object> */
1187 private function computeInsertExecutionOrder(): array
1188 {
1189 $sort = new TopologicalSort();
1190
1191 // First make sure we have all the nodes
1192 foreach ($this->entityInsertions as $entity) {
1193 $sort->addNode($entity);
1194 }
1195
1196 // Now add edges
1197 foreach ($this->entityInsertions as $entity) {
1198 $class = $this->em->getClassMetadata($entity::class);
1199
1200 foreach ($class->associationMappings as $assoc) {
1201 // We only need to consider the owning sides of to-one associations,
1202 // since many-to-many associations are persisted at a later step and
1203 // have no insertion order problems (all entities already in the database
1204 // at that time).
1205 if (! $assoc->isToOneOwningSide()) {
1206 continue;
1207 }
1208
1209 $targetEntity = $class->getFieldValue($entity, $assoc->fieldName);
1210
1211 // If there is no entity that we need to refer to, or it is already in the
1212 // database (i. e. does not have to be inserted), no need to consider it.
1213 if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
1214 continue;
1215 }
1216
1217 // An entity that references back to itself _and_ uses an application-provided ID
1218 // (the "NONE" generator strategy) can be exempted from commit order computation.
1219 // See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case.
1220 // A non-NULLable self-reference would be a cycle in the graph.
1221 if ($targetEntity === $entity && $class->isIdentifierNatural()) {
1222 continue;
1223 }
1224
1225 // According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn,
1226 // the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other
1227 // level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well.
1228 //
1229 // Same in \Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns,
1230 // to give two examples.
1231 $joinColumns = reset($assoc->joinColumns);
1232 $isNullable = ! isset($joinColumns->nullable) || $joinColumns->nullable;
1233
1234 // Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
1235 // topological sort result will output the depended-upon nodes first, which means we can insert
1236 // entities in that order.
1237 $sort->addEdge($entity, $targetEntity, $isNullable);
1238 }
1239 }
1240
1241 return $sort->sort();
1242 }
1243
1244 /** @return list<object> */
1245 private function computeDeleteExecutionOrder(): array
1246 {
1247 $stronglyConnectedComponents = new StronglyConnectedComponents();
1248 $sort = new TopologicalSort();
1249
1250 foreach ($this->entityDeletions as $entity) {
1251 $stronglyConnectedComponents->addNode($entity);
1252 $sort->addNode($entity);
1253 }
1254
1255 // First, consider only "on delete cascade" associations between entities
1256 // and find strongly connected groups. Once we delete any one of the entities
1257 // in such a group, _all_ of the other entities will be removed as well. So,
1258 // we need to treat those groups like a single entity when performing delete
1259 // order topological sorting.
1260 foreach ($this->entityDeletions as $entity) {
1261 $class = $this->em->getClassMetadata($entity::class);
1262
1263 foreach ($class->associationMappings as $assoc) {
1264 // We only need to consider the owning sides of to-one associations,
1265 // since many-to-many associations can always be (and have already been)
1266 // deleted in a preceding step.
1267 if (! $assoc->isToOneOwningSide()) {
1268 continue;
1269 }
1270
1271 $joinColumns = reset($assoc->joinColumns);
1272 if (! isset($joinColumns->onDelete)) {
1273 continue;
1274 }
1275
1276 $onDeleteOption = strtolower($joinColumns->onDelete);
1277 if ($onDeleteOption !== 'cascade') {
1278 continue;
1279 }
1280
1281 $targetEntity = $class->getFieldValue($entity, $assoc->fieldName);
1282
1283 // If the association does not refer to another entity or that entity
1284 // is not to be deleted, there is no ordering problem and we can
1285 // skip this particular association.
1286 if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
1287 continue;
1288 }
1289
1290 $stronglyConnectedComponents->addEdge($entity, $targetEntity);
1291 }
1292 }
1293
1294 $stronglyConnectedComponents->findStronglyConnectedComponents();
1295
1296 // Now do the actual topological sorting to find the delete order.
1297 foreach ($this->entityDeletions as $entity) {
1298 $class = $this->em->getClassMetadata($entity::class);
1299
1300 // Get the entities representing the SCC
1301 $entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
1302
1303 // When $entity is part of a non-trivial strongly connected component group
1304 // (a group containing not only those entities alone), make sure we process it _after_ the
1305 // entity representing the group.
1306 // The dependency direction implies that "$entity depends on $entityComponent
1307 // being deleted first". The topological sort will output the depended-upon nodes first.
1308 if ($entityComponent !== $entity) {
1309 $sort->addEdge($entity, $entityComponent, false);
1310 }
1311
1312 foreach ($class->associationMappings as $assoc) {
1313 // We only need to consider the owning sides of to-one associations,
1314 // since many-to-many associations can always be (and have already been)
1315 // deleted in a preceding step.
1316 if (! $assoc->isToOneOwningSide()) {
1317 continue;
1318 }
1319
1320 // For associations that implement a database-level set null operation,
1321 // we do not have to follow a particular order: If the referred-to entity is
1322 // deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
1323 // So, we can skip it in the computation.
1324 $joinColumns = reset($assoc->joinColumns);
1325 if (isset($joinColumns->onDelete)) {
1326 $onDeleteOption = strtolower($joinColumns->onDelete);
1327 if ($onDeleteOption === 'set null') {
1328 continue;
1329 }
1330 }
1331
1332 $targetEntity = $class->getFieldValue($entity, $assoc->fieldName);
1333
1334 // If the association does not refer to another entity or that entity
1335 // is not to be deleted, there is no ordering problem and we can
1336 // skip this particular association.
1337 if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
1338 continue;
1339 }
1340
1341 // Get the entities representing the SCC
1342 $targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
1343
1344 // When we have a dependency between two different groups of strongly connected nodes,
1345 // add it to the computation.
1346 // The dependency direction implies that "$targetEntityComponent depends on $entityComponent
1347 // being deleted first". The topological sort will output the depended-upon nodes first,
1348 // so we can work through the result in the returned order.
1349 if ($targetEntityComponent !== $entityComponent) {
1350 $sort->addEdge($targetEntityComponent, $entityComponent, false);
1351 }
1352 }
1353 }
1354
1355 return $sort->sort();
1356 }
1357
1358 /**
1359 * Schedules an entity for insertion into the database.
1360 * If the entity already has an identifier, it will be added to the identity map.
1361 *
1362 * @throws ORMInvalidArgumentException
1363 * @throws InvalidArgumentException
1364 */
1365 public function scheduleForInsert(object $entity): void
1366 {
1367 $oid = spl_object_id($entity);
1368
1369 if (isset($this->entityUpdates[$oid])) {
1370 throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1371 }
1372
1373 if (isset($this->entityDeletions[$oid])) {
1374 throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1375 }
1376
1377 if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1378 throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1379 }
1380
1381 if (isset($this->entityInsertions[$oid])) {
1382 throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1383 }
1384
1385 $this->entityInsertions[$oid] = $entity;
1386
1387 if (isset($this->entityIdentifiers[$oid])) {
1388 $this->addToIdentityMap($entity);
1389 }
1390 }
1391
1392 /**
1393 * Checks whether an entity is scheduled for insertion.
1394 */
1395 public function isScheduledForInsert(object $entity): bool
1396 {
1397 return isset($this->entityInsertions[spl_object_id($entity)]);
1398 }
1399
1400 /**
1401 * Schedules an entity for being updated.
1402 *
1403 * @throws ORMInvalidArgumentException
1404 */
1405 public function scheduleForUpdate(object $entity): void
1406 {
1407 $oid = spl_object_id($entity);
1408
1409 if (! isset($this->entityIdentifiers[$oid])) {
1410 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
1411 }
1412
1413 if (isset($this->entityDeletions[$oid])) {
1414 throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
1415 }
1416
1417 if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1418 $this->entityUpdates[$oid] = $entity;
1419 }
1420 }
1421
1422 /**
1423 * INTERNAL:
1424 * Schedules an extra update that will be executed immediately after the
1425 * regular entity updates within the currently running commit cycle.
1426 *
1427 * Extra updates for entities are stored as (entity, changeset) tuples.
1428 *
1429 * @psalm-param array<string, array{mixed, mixed}> $changeset The changeset of the entity (what to update).
1430 *
1431 * @ignore
1432 */
1433 public function scheduleExtraUpdate(object $entity, array $changeset): void
1434 {
1435 $oid = spl_object_id($entity);
1436 $extraUpdate = [$entity, $changeset];
1437
1438 if (isset($this->extraUpdates[$oid])) {
1439 [, $changeset2] = $this->extraUpdates[$oid];
1440
1441 $extraUpdate = [$entity, $changeset + $changeset2];
1442 }
1443
1444 $this->extraUpdates[$oid] = $extraUpdate;
1445 }
1446
1447 /**
1448 * Checks whether an entity is registered as dirty in the unit of work.
1449 * Note: Is not very useful currently as dirty entities are only registered
1450 * at commit time.
1451 */
1452 public function isScheduledForUpdate(object $entity): bool
1453 {
1454 return isset($this->entityUpdates[spl_object_id($entity)]);
1455 }
1456
1457 /**
1458 * Checks whether an entity is registered to be checked in the unit of work.
1459 */
1460 public function isScheduledForDirtyCheck(object $entity): bool
1461 {
1462 $rootEntityName = $this->em->getClassMetadata($entity::class)->rootEntityName;
1463
1464 return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
1465 }
1466
1467 /**
1468 * INTERNAL:
1469 * Schedules an entity for deletion.
1470 */
1471 public function scheduleForDelete(object $entity): void
1472 {
1473 $oid = spl_object_id($entity);
1474
1475 if (isset($this->entityInsertions[$oid])) {
1476 if ($this->isInIdentityMap($entity)) {
1477 $this->removeFromIdentityMap($entity);
1478 }
1479
1480 unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1481
1482 return; // entity has not been persisted yet, so nothing more to do.
1483 }
1484
1485 if (! $this->isInIdentityMap($entity)) {
1486 return;
1487 }
1488
1489 unset($this->entityUpdates[$oid]);
1490
1491 if (! isset($this->entityDeletions[$oid])) {
1492 $this->entityDeletions[$oid] = $entity;
1493 $this->entityStates[$oid] = self::STATE_REMOVED;
1494 }
1495 }
1496
1497 /**
1498 * Checks whether an entity is registered as removed/deleted with the unit
1499 * of work.
1500 */
1501 public function isScheduledForDelete(object $entity): bool
1502 {
1503 return isset($this->entityDeletions[spl_object_id($entity)]);
1504 }
1505
1506 /**
1507 * Checks whether an entity is scheduled for insertion, update or deletion.
1508 */
1509 public function isEntityScheduled(object $entity): bool
1510 {
1511 $oid = spl_object_id($entity);
1512
1513 return isset($this->entityInsertions[$oid])
1514 || isset($this->entityUpdates[$oid])
1515 || isset($this->entityDeletions[$oid]);
1516 }
1517
1518 /**
1519 * INTERNAL:
1520 * Registers an entity in the identity map.
1521 * Note that entities in a hierarchy are registered with the class name of
1522 * the root entity.
1523 *
1524 * @return bool TRUE if the registration was successful, FALSE if the identity of
1525 * the entity in question is already managed.
1526 *
1527 * @throws ORMInvalidArgumentException
1528 * @throws EntityIdentityCollisionException
1529 *
1530 * @ignore
1531 */
1532 public function addToIdentityMap(object $entity): bool
1533 {
1534 $classMetadata = $this->em->getClassMetadata($entity::class);
1535 $idHash = $this->getIdHashByEntity($entity);
1536 $className = $classMetadata->rootEntityName;
1537
1538 if (isset($this->identityMap[$className][$idHash])) {
1539 if ($this->identityMap[$className][$idHash] !== $entity) {
1540 throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash);
1541 }
1542
1543 return false;
1544 }
1545
1546 $this->identityMap[$className][$idHash] = $entity;
1547
1548 return true;
1549 }
1550
1551 /**
1552 * Gets the id hash of an entity by its identifier.
1553 *
1554 * @param array<string|int, mixed> $identifier The identifier of an entity
1555 *
1556 * @return string The entity id hash.
1557 */
1558 final public static function getIdHashByIdentifier(array $identifier): string
1559 {
1560 foreach ($identifier as $k => $value) {
1561 if ($value instanceof BackedEnum) {
1562 $identifier[$k] = $value->value;
1563 }
1564 }
1565
1566 return implode(
1567 ' ',
1568 $identifier,
1569 );
1570 }
1571
1572 /**
1573 * Gets the id hash of an entity.
1574 *
1575 * @param object $entity The entity managed by Unit Of Work
1576 *
1577 * @return string The entity id hash.
1578 */
1579 public function getIdHashByEntity(object $entity): string
1580 {
1581 $identifier = $this->entityIdentifiers[spl_object_id($entity)];
1582
1583 if (empty($identifier) || in_array(null, $identifier, true)) {
1584 $classMetadata = $this->em->getClassMetadata($entity::class);
1585
1586 throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
1587 }
1588
1589 return self::getIdHashByIdentifier($identifier);
1590 }
1591
1592 /**
1593 * Gets the state of an entity with regard to the current unit of work.
1594 *
1595 * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1596 * This parameter can be set to improve performance of entity state detection
1597 * by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1598 * is either known or does not matter for the caller of the method.
1599 * @psalm-param self::STATE_*|null $assume
1600 *
1601 * @psalm-return self::STATE_*
1602 */
1603 public function getEntityState(object $entity, int|null $assume = null): int
1604 {
1605 $oid = spl_object_id($entity);
1606
1607 if (isset($this->entityStates[$oid])) {
1608 return $this->entityStates[$oid];
1609 }
1610
1611 if ($assume !== null) {
1612 return $assume;
1613 }
1614
1615 // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1616 // Note that you can not remember the NEW or DETACHED state in _entityStates since
1617 // the UoW does not hold references to such objects and the object hash can be reused.
1618 // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1619 $class = $this->em->getClassMetadata($entity::class);
1620 $id = $class->getIdentifierValues($entity);
1621
1622 if (! $id) {
1623 return self::STATE_NEW;
1624 }
1625
1626 if ($class->containsForeignIdentifier || $class->containsEnumIdentifier) {
1627 $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1628 }
1629
1630 switch (true) {
1631 case $class->isIdentifierNatural():
1632 // Check for a version field, if available, to avoid a db lookup.
1633 if ($class->isVersioned) {
1634 assert($class->versionField !== null);
1635
1636 return $class->getFieldValue($entity, $class->versionField)
1637 ? self::STATE_DETACHED
1638 : self::STATE_NEW;
1639 }
1640
1641 // Last try before db lookup: check the identity map.
1642 if ($this->tryGetById($id, $class->rootEntityName)) {
1643 return self::STATE_DETACHED;
1644 }
1645
1646 // db lookup
1647 if ($this->getEntityPersister($class->name)->exists($entity)) {
1648 return self::STATE_DETACHED;
1649 }
1650
1651 return self::STATE_NEW;
1652
1653 case ! $class->idGenerator->isPostInsertGenerator():
1654 // if we have a pre insert generator we can't be sure that having an id
1655 // really means that the entity exists. We have to verify this through
1656 // the last resort: a db lookup
1657
1658 // Last try before db lookup: check the identity map.
1659 if ($this->tryGetById($id, $class->rootEntityName)) {
1660 return self::STATE_DETACHED;
1661 }
1662
1663 // db lookup
1664 if ($this->getEntityPersister($class->name)->exists($entity)) {
1665 return self::STATE_DETACHED;
1666 }
1667
1668 return self::STATE_NEW;
1669
1670 default:
1671 return self::STATE_DETACHED;
1672 }
1673 }
1674
1675 /**
1676 * INTERNAL:
1677 * Removes an entity from the identity map. This effectively detaches the
1678 * entity from the persistence management of Doctrine.
1679 *
1680 * @throws ORMInvalidArgumentException
1681 *
1682 * @ignore
1683 */
1684 public function removeFromIdentityMap(object $entity): bool
1685 {
1686 $oid = spl_object_id($entity);
1687 $classMetadata = $this->em->getClassMetadata($entity::class);
1688 $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
1689
1690 if ($idHash === '') {
1691 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1692 }
1693
1694 $className = $classMetadata->rootEntityName;
1695
1696 if (isset($this->identityMap[$className][$idHash])) {
1697 unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
1698
1699 //$this->entityStates[$oid] = self::STATE_DETACHED;
1700
1701 return true;
1702 }
1703
1704 return false;
1705 }
1706
1707 /**
1708 * INTERNAL:
1709 * Gets an entity in the identity map by its identifier hash.
1710 *
1711 * @ignore
1712 */
1713 public function getByIdHash(string $idHash, string $rootClassName): object|null
1714 {
1715 return $this->identityMap[$rootClassName][$idHash];
1716 }
1717
1718 /**
1719 * INTERNAL:
1720 * Tries to get an entity by its identifier hash. If no entity is found for
1721 * the given hash, FALSE is returned.
1722 *
1723 * @param mixed $idHash (must be possible to cast it to string)
1724 *
1725 * @return false|object The found entity or FALSE.
1726 *
1727 * @ignore
1728 */
1729 public function tryGetByIdHash(mixed $idHash, string $rootClassName): object|false
1730 {
1731 $stringIdHash = (string) $idHash;
1732
1733 return $this->identityMap[$rootClassName][$stringIdHash] ?? false;
1734 }
1735
1736 /**
1737 * Checks whether an entity is registered in the identity map of this UnitOfWork.
1738 */
1739 public function isInIdentityMap(object $entity): bool
1740 {
1741 $oid = spl_object_id($entity);
1742
1743 if (empty($this->entityIdentifiers[$oid])) {
1744 return false;
1745 }
1746
1747 $classMetadata = $this->em->getClassMetadata($entity::class);
1748 $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
1749
1750 return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
1751 }
1752
1753 /**
1754 * Persists an entity as part of the current unit of work.
1755 */
1756 public function persist(object $entity): void
1757 {
1758 $visited = [];
1759
1760 $this->doPersist($entity, $visited);
1761 }
1762
1763 /**
1764 * Persists an entity as part of the current unit of work.
1765 *
1766 * This method is internally called during persist() cascades as it tracks
1767 * the already visited entities to prevent infinite recursions.
1768 *
1769 * @psalm-param array<int, object> $visited The already visited entities.
1770 *
1771 * @throws ORMInvalidArgumentException
1772 * @throws UnexpectedValueException
1773 */
1774 private function doPersist(object $entity, array &$visited): void
1775 {
1776 $oid = spl_object_id($entity);
1777
1778 if (isset($visited[$oid])) {
1779 return; // Prevent infinite recursion
1780 }
1781
1782 $visited[$oid] = $entity; // Mark visited
1783
1784 $class = $this->em->getClassMetadata($entity::class);
1785
1786 // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1787 // If we would detect DETACHED here we would throw an exception anyway with the same
1788 // consequences (not recoverable/programming error), so just assuming NEW here
1789 // lets us avoid some database lookups for entities with natural identifiers.
1790 $entityState = $this->getEntityState($entity, self::STATE_NEW);
1791
1792 switch ($entityState) {
1793 case self::STATE_MANAGED:
1794 // Nothing to do, except if policy is "deferred explicit"
1795 if ($class->isChangeTrackingDeferredExplicit()) {
1796 $this->scheduleForDirtyCheck($entity);
1797 }
1798
1799 break;
1800
1801 case self::STATE_NEW:
1802 $this->persistNew($class, $entity);
1803 break;
1804
1805 case self::STATE_REMOVED:
1806 // Entity becomes managed again
1807 unset($this->entityDeletions[$oid]);
1808 $this->addToIdentityMap($entity);
1809
1810 $this->entityStates[$oid] = self::STATE_MANAGED;
1811
1812 if ($class->isChangeTrackingDeferredExplicit()) {
1813 $this->scheduleForDirtyCheck($entity);
1814 }
1815
1816 break;
1817
1818 case self::STATE_DETACHED:
1819 // Can actually not happen right now since we assume STATE_NEW.
1820 throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1821
1822 default:
1823 throw new UnexpectedValueException(sprintf(
1824 'Unexpected entity state: %s. %s',
1825 $entityState,
1826 self::objToStr($entity),
1827 ));
1828 }
1829
1830 $this->cascadePersist($entity, $visited);
1831 }
1832
1833 /**
1834 * Deletes an entity as part of the current unit of work.
1835 */
1836 public function remove(object $entity): void
1837 {
1838 $visited = [];
1839
1840 $this->doRemove($entity, $visited);
1841 }
1842
1843 /**
1844 * Deletes an entity as part of the current unit of work.
1845 *
1846 * This method is internally called during delete() cascades as it tracks
1847 * the already visited entities to prevent infinite recursions.
1848 *
1849 * @psalm-param array<int, object> $visited The map of the already visited entities.
1850 *
1851 * @throws ORMInvalidArgumentException If the instance is a detached entity.
1852 * @throws UnexpectedValueException
1853 */
1854 private function doRemove(object $entity, array &$visited): void
1855 {
1856 $oid = spl_object_id($entity);
1857
1858 if (isset($visited[$oid])) {
1859 return; // Prevent infinite recursion
1860 }
1861
1862 $visited[$oid] = $entity; // mark visited
1863
1864 // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1865 // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1866 $this->cascadeRemove($entity, $visited);
1867
1868 $class = $this->em->getClassMetadata($entity::class);
1869 $entityState = $this->getEntityState($entity);
1870
1871 switch ($entityState) {
1872 case self::STATE_NEW:
1873 case self::STATE_REMOVED:
1874 // nothing to do
1875 break;
1876
1877 case self::STATE_MANAGED:
1878 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1879
1880 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1881 $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new PreRemoveEventArgs($entity, $this->em), $invoke);
1882 }
1883
1884 $this->scheduleForDelete($entity);
1885 break;
1886
1887 case self::STATE_DETACHED:
1888 throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1889
1890 default:
1891 throw new UnexpectedValueException(sprintf(
1892 'Unexpected entity state: %s. %s',
1893 $entityState,
1894 self::objToStr($entity),
1895 ));
1896 }
1897 }
1898
1899 /**
1900 * Detaches an entity from the persistence management. It's persistence will
1901 * no longer be managed by Doctrine.
1902 */
1903 public function detach(object $entity): void
1904 {
1905 $visited = [];
1906
1907 $this->doDetach($entity, $visited);
1908 }
1909
1910 /**
1911 * Executes a detach operation on the given entity.
1912 *
1913 * @param mixed[] $visited
1914 * @param bool $noCascade if true, don't cascade detach operation.
1915 */
1916 private function doDetach(
1917 object $entity,
1918 array &$visited,
1919 bool $noCascade = false,
1920 ): void {
1921 $oid = spl_object_id($entity);
1922
1923 if (isset($visited[$oid])) {
1924 return; // Prevent infinite recursion
1925 }
1926
1927 $visited[$oid] = $entity; // mark visited
1928
1929 switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1930 case self::STATE_MANAGED:
1931 if ($this->isInIdentityMap($entity)) {
1932 $this->removeFromIdentityMap($entity);
1933 }
1934
1935 unset(
1936 $this->entityInsertions[$oid],
1937 $this->entityUpdates[$oid],
1938 $this->entityDeletions[$oid],
1939 $this->entityIdentifiers[$oid],
1940 $this->entityStates[$oid],
1941 $this->originalEntityData[$oid],
1942 );
1943 break;
1944 case self::STATE_NEW:
1945 case self::STATE_DETACHED:
1946 return;
1947 }
1948
1949 if (! $noCascade) {
1950 $this->cascadeDetach($entity, $visited);
1951 }
1952 }
1953
1954 /**
1955 * Refreshes the state of the given entity from the database, overwriting
1956 * any local, unpersisted changes.
1957 *
1958 * @psalm-param LockMode::*|null $lockMode
1959 *
1960 * @throws InvalidArgumentException If the entity is not MANAGED.
1961 * @throws TransactionRequiredException
1962 */
1963 public function refresh(object $entity, LockMode|int|null $lockMode = null): void
1964 {
1965 $visited = [];
1966
1967 $this->doRefresh($entity, $visited, $lockMode);
1968 }
1969
1970 /**
1971 * Executes a refresh operation on an entity.
1972 *
1973 * @psalm-param array<int, object> $visited The already visited entities during cascades.
1974 * @psalm-param LockMode::*|null $lockMode
1975 *
1976 * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1977 * @throws TransactionRequiredException
1978 */
1979 private function doRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void
1980 {
1981 switch (true) {
1982 case $lockMode === LockMode::PESSIMISTIC_READ:
1983 case $lockMode === LockMode::PESSIMISTIC_WRITE:
1984 if (! $this->em->getConnection()->isTransactionActive()) {
1985 throw TransactionRequiredException::transactionRequired();
1986 }
1987 }
1988
1989 $oid = spl_object_id($entity);
1990
1991 if (isset($visited[$oid])) {
1992 return; // Prevent infinite recursion
1993 }
1994
1995 $visited[$oid] = $entity; // mark visited
1996
1997 $class = $this->em->getClassMetadata($entity::class);
1998
1999 if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2000 throw ORMInvalidArgumentException::entityNotManaged($entity);
2001 }
2002
2003 $this->getEntityPersister($class->name)->refresh(
2004 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2005 $entity,
2006 $lockMode,
2007 );
2008
2009 $this->cascadeRefresh($entity, $visited, $lockMode);
2010 }
2011
2012 /**
2013 * Cascades a refresh operation to associated entities.
2014 *
2015 * @psalm-param array<int, object> $visited
2016 * @psalm-param LockMode::*|null $lockMode
2017 */
2018 private function cascadeRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void
2019 {
2020 $class = $this->em->getClassMetadata($entity::class);
2021
2022 $associationMappings = array_filter(
2023 $class->associationMappings,
2024 static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRefresh()
2025 );
2026
2027 foreach ($associationMappings as $assoc) {
2028 $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
2029
2030 switch (true) {
2031 case $relatedEntities instanceof PersistentCollection:
2032 // Unwrap so that foreach() does not initialize
2033 $relatedEntities = $relatedEntities->unwrap();
2034 // break; is commented intentionally!
2035
2036 case $relatedEntities instanceof Collection:
2037 case is_array($relatedEntities):
2038 foreach ($relatedEntities as $relatedEntity) {
2039 $this->doRefresh($relatedEntity, $visited, $lockMode);
2040 }
2041
2042 break;
2043
2044 case $relatedEntities !== null:
2045 $this->doRefresh($relatedEntities, $visited, $lockMode);
2046 break;
2047
2048 default:
2049 // Do nothing
2050 }
2051 }
2052 }
2053
2054 /**
2055 * Cascades a detach operation to associated entities.
2056 *
2057 * @param array<int, object> $visited
2058 */
2059 private function cascadeDetach(object $entity, array &$visited): void
2060 {
2061 $class = $this->em->getClassMetadata($entity::class);
2062
2063 $associationMappings = array_filter(
2064 $class->associationMappings,
2065 static fn (AssociationMapping $assoc): bool => $assoc->isCascadeDetach()
2066 );
2067
2068 foreach ($associationMappings as $assoc) {
2069 $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
2070
2071 switch (true) {
2072 case $relatedEntities instanceof PersistentCollection:
2073 // Unwrap so that foreach() does not initialize
2074 $relatedEntities = $relatedEntities->unwrap();
2075 // break; is commented intentionally!
2076
2077 case $relatedEntities instanceof Collection:
2078 case is_array($relatedEntities):
2079 foreach ($relatedEntities as $relatedEntity) {
2080 $this->doDetach($relatedEntity, $visited);
2081 }
2082
2083 break;
2084
2085 case $relatedEntities !== null:
2086 $this->doDetach($relatedEntities, $visited);
2087 break;
2088
2089 default:
2090 // Do nothing
2091 }
2092 }
2093 }
2094
2095 /**
2096 * Cascades the save operation to associated entities.
2097 *
2098 * @psalm-param array<int, object> $visited
2099 */
2100 private function cascadePersist(object $entity, array &$visited): void
2101 {
2102 if ($this->isUninitializedObject($entity)) {
2103 // nothing to do - proxy is not initialized, therefore we don't do anything with it
2104 return;
2105 }
2106
2107 $class = $this->em->getClassMetadata($entity::class);
2108
2109 $associationMappings = array_filter(
2110 $class->associationMappings,
2111 static fn (AssociationMapping $assoc): bool => $assoc->isCascadePersist()
2112 );
2113
2114 foreach ($associationMappings as $assoc) {
2115 $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
2116
2117 switch (true) {
2118 case $relatedEntities instanceof PersistentCollection:
2119 // Unwrap so that foreach() does not initialize
2120 $relatedEntities = $relatedEntities->unwrap();
2121 // break; is commented intentionally!
2122
2123 case $relatedEntities instanceof Collection:
2124 case is_array($relatedEntities):
2125 if ($assoc->isToMany() <= 0) {
2126 throw ORMInvalidArgumentException::invalidAssociation(
2127 $this->em->getClassMetadata($assoc->targetEntity),
2128 $assoc,
2129 $relatedEntities,
2130 );
2131 }
2132
2133 foreach ($relatedEntities as $relatedEntity) {
2134 $this->doPersist($relatedEntity, $visited);
2135 }
2136
2137 break;
2138
2139 case $relatedEntities !== null:
2140 if (! $relatedEntities instanceof $assoc->targetEntity) {
2141 throw ORMInvalidArgumentException::invalidAssociation(
2142 $this->em->getClassMetadata($assoc->targetEntity),
2143 $assoc,
2144 $relatedEntities,
2145 );
2146 }
2147
2148 $this->doPersist($relatedEntities, $visited);
2149 break;
2150
2151 default:
2152 // Do nothing
2153 }
2154 }
2155 }
2156
2157 /**
2158 * Cascades the delete operation to associated entities.
2159 *
2160 * @psalm-param array<int, object> $visited
2161 */
2162 private function cascadeRemove(object $entity, array &$visited): void
2163 {
2164 $class = $this->em->getClassMetadata($entity::class);
2165
2166 $associationMappings = array_filter(
2167 $class->associationMappings,
2168 static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRemove()
2169 );
2170
2171 if ($associationMappings) {
2172 $this->initializeObject($entity);
2173 }
2174
2175 $entitiesToCascade = [];
2176
2177 foreach ($associationMappings as $assoc) {
2178 $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity);
2179
2180 switch (true) {
2181 case $relatedEntities instanceof Collection:
2182 case is_array($relatedEntities):
2183 // If its a PersistentCollection initialization is intended! No unwrap!
2184 foreach ($relatedEntities as $relatedEntity) {
2185 $entitiesToCascade[] = $relatedEntity;
2186 }
2187
2188 break;
2189
2190 case $relatedEntities !== null:
2191 $entitiesToCascade[] = $relatedEntities;
2192 break;
2193
2194 default:
2195 // Do nothing
2196 }
2197 }
2198
2199 foreach ($entitiesToCascade as $relatedEntity) {
2200 $this->doRemove($relatedEntity, $visited);
2201 }
2202 }
2203
2204 /**
2205 * Acquire a lock on the given entity.
2206 *
2207 * @psalm-param LockMode::* $lockMode
2208 *
2209 * @throws ORMInvalidArgumentException
2210 * @throws TransactionRequiredException
2211 * @throws OptimisticLockException
2212 */
2213 public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void
2214 {
2215 if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2216 throw ORMInvalidArgumentException::entityNotManaged($entity);
2217 }
2218
2219 $class = $this->em->getClassMetadata($entity::class);
2220
2221 switch (true) {
2222 case $lockMode === LockMode::OPTIMISTIC:
2223 if (! $class->isVersioned) {
2224 throw OptimisticLockException::notVersioned($class->name);
2225 }
2226
2227 if ($lockVersion === null) {
2228 return;
2229 }
2230
2231 $this->initializeObject($entity);
2232
2233 assert($class->versionField !== null);
2234 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
2235
2236 // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
2237 if ($entityVersion != $lockVersion) {
2238 throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2239 }
2240
2241 break;
2242
2243 case $lockMode === LockMode::NONE:
2244 case $lockMode === LockMode::PESSIMISTIC_READ:
2245 case $lockMode === LockMode::PESSIMISTIC_WRITE:
2246 if (! $this->em->getConnection()->isTransactionActive()) {
2247 throw TransactionRequiredException::transactionRequired();
2248 }
2249
2250 $oid = spl_object_id($entity);
2251
2252 $this->getEntityPersister($class->name)->lock(
2253 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2254 $lockMode,
2255 );
2256 break;
2257
2258 default:
2259 // Do nothing
2260 }
2261 }
2262
2263 /**
2264 * Clears the UnitOfWork.
2265 */
2266 public function clear(): void
2267 {
2268 $this->identityMap =
2269 $this->entityIdentifiers =
2270 $this->originalEntityData =
2271 $this->entityChangeSets =
2272 $this->entityStates =
2273 $this->scheduledForSynchronization =
2274 $this->entityInsertions =
2275 $this->entityUpdates =
2276 $this->entityDeletions =
2277 $this->nonCascadedNewDetectedEntities =
2278 $this->collectionDeletions =
2279 $this->collectionUpdates =
2280 $this->extraUpdates =
2281 $this->readOnlyObjects =
2282 $this->pendingCollectionElementRemovals =
2283 $this->visitedCollections =
2284 $this->eagerLoadingEntities =
2285 $this->eagerLoadingCollections =
2286 $this->orphanRemovals = [];
2287
2288 if ($this->evm->hasListeners(Events::onClear)) {
2289 $this->evm->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em));
2290 }
2291 }
2292
2293 /**
2294 * INTERNAL:
2295 * Schedules an orphaned entity for removal. The remove() operation will be
2296 * invoked on that entity at the beginning of the next commit of this
2297 * UnitOfWork.
2298 *
2299 * @ignore
2300 */
2301 public function scheduleOrphanRemoval(object $entity): void
2302 {
2303 $this->orphanRemovals[spl_object_id($entity)] = $entity;
2304 }
2305
2306 /**
2307 * INTERNAL:
2308 * Cancels a previously scheduled orphan removal.
2309 *
2310 * @ignore
2311 */
2312 public function cancelOrphanRemoval(object $entity): void
2313 {
2314 unset($this->orphanRemovals[spl_object_id($entity)]);
2315 }
2316
2317 /**
2318 * INTERNAL:
2319 * Schedules a complete collection for removal when this UnitOfWork commits.
2320 */
2321 public function scheduleCollectionDeletion(PersistentCollection $coll): void
2322 {
2323 $coid = spl_object_id($coll);
2324
2325 // TODO: if $coll is already scheduled for recreation ... what to do?
2326 // Just remove $coll from the scheduled recreations?
2327 unset($this->collectionUpdates[$coid]);
2328
2329 $this->collectionDeletions[$coid] = $coll;
2330 }
2331
2332 public function isCollectionScheduledForDeletion(PersistentCollection $coll): bool
2333 {
2334 return isset($this->collectionDeletions[spl_object_id($coll)]);
2335 }
2336
2337 /**
2338 * INTERNAL:
2339 * Creates an entity. Used for reconstitution of persistent entities.
2340 *
2341 * Internal note: Highly performance-sensitive method.
2342 *
2343 * @param string $className The name of the entity class.
2344 * @param mixed[] $data The data for the entity.
2345 * @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity.
2346 * @psalm-param class-string $className
2347 * @psalm-param array<string, mixed> $hints
2348 *
2349 * @return object The managed entity instance.
2350 *
2351 * @ignore
2352 * @todo Rename: getOrCreateEntity
2353 */
2354 public function createEntity(string $className, array $data, array &$hints = []): object
2355 {
2356 $class = $this->em->getClassMetadata($className);
2357
2358 $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2359 $idHash = self::getIdHashByIdentifier($id);
2360
2361 if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
2362 $entity = $this->identityMap[$class->rootEntityName][$idHash];
2363 $oid = spl_object_id($entity);
2364
2365 if (
2366 isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])
2367 ) {
2368 $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
2369 if (
2370 $unmanagedProxy !== $entity
2371 && $this->isIdentifierEquals($unmanagedProxy, $entity)
2372 ) {
2373 // We will hydrate the given un-managed proxy anyway:
2374 // continue work, but consider it the entity from now on
2375 $entity = $unmanagedProxy;
2376 }
2377 }
2378
2379 if ($this->isUninitializedObject($entity)) {
2380 $entity->__setInitialized(true);
2381 } else {
2382 if (
2383 ! isset($hints[Query::HINT_REFRESH])
2384 || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)
2385 ) {
2386 return $entity;
2387 }
2388 }
2389
2390 $this->originalEntityData[$oid] = $data;
2391 } else {
2392 $entity = $class->newInstance();
2393 $oid = spl_object_id($entity);
2394 $this->registerManaged($entity, $id, $data);
2395
2396 if (isset($hints[Query::HINT_READ_ONLY])) {
2397 $this->readOnlyObjects[$oid] = true;
2398 }
2399 }
2400
2401 foreach ($data as $field => $value) {
2402 if (isset($class->fieldMappings[$field])) {
2403 $class->reflFields[$field]->setValue($entity, $value);
2404 }
2405 }
2406
2407 // Loading the entity right here, if its in the eager loading map get rid of it there.
2408 unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2409
2410 if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2411 unset($this->eagerLoadingEntities[$class->rootEntityName]);
2412 }
2413
2414 foreach ($class->associationMappings as $field => $assoc) {
2415 // Check if the association is not among the fetch-joined associations already.
2416 if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2417 continue;
2418 }
2419
2420 if (! isset($hints['fetchMode'][$class->name][$field])) {
2421 $hints['fetchMode'][$class->name][$field] = $assoc->fetch;
2422 }
2423
2424 $targetClass = $this->em->getClassMetadata($assoc->targetEntity);
2425
2426 switch (true) {
2427 case $assoc->isToOne():
2428 if (! $assoc->isOwningSide()) {
2429 // use the given entity association
2430 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2431 $this->originalEntityData[$oid][$field] = $data[$field];
2432
2433 $class->reflFields[$field]->setValue($entity, $data[$field]);
2434 $targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity);
2435
2436 continue 2;
2437 }
2438
2439 // Inverse side of x-to-one can never be lazy
2440 $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity));
2441
2442 continue 2;
2443 }
2444
2445 // use the entity association
2446 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2447 $class->reflFields[$field]->setValue($entity, $data[$field]);
2448 $this->originalEntityData[$oid][$field] = $data[$field];
2449
2450 break;
2451 }
2452
2453 $associatedId = [];
2454
2455 assert($assoc->isToOneOwningSide());
2456 // TODO: Is this even computed right in all cases of composite keys?
2457 foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $srcColumn) {
2458 $joinColumnValue = $data[$srcColumn] ?? null;
2459
2460 if ($joinColumnValue !== null) {
2461 if ($joinColumnValue instanceof BackedEnum) {
2462 $joinColumnValue = $joinColumnValue->value;
2463 }
2464
2465 if ($targetClass->containsForeignIdentifier) {
2466 $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
2467 } else {
2468 $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
2469 }
2470 } elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) {
2471 // the missing key is part of target's entity primary key
2472 $associatedId = [];
2473 break;
2474 }
2475 }
2476
2477 if (! $associatedId) {
2478 // Foreign key is NULL
2479 $class->reflFields[$field]->setValue($entity, null);
2480 $this->originalEntityData[$oid][$field] = null;
2481
2482 break;
2483 }
2484
2485 // Foreign key is set
2486 // Check identity map first
2487 // FIXME: Can break easily with composite keys if join column values are in
2488 // wrong order. The correct order is the one in ClassMetadata#identifier.
2489 $relatedIdHash = self::getIdHashByIdentifier($associatedId);
2490
2491 switch (true) {
2492 case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
2493 $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2494
2495 // If this is an uninitialized proxy, we are deferring eager loads,
2496 // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2497 // then we can append this entity for eager loading!
2498 if (
2499 $hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER &&
2500 isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2501 ! $targetClass->isIdentifierComposite &&
2502 $this->isUninitializedObject($newValue)
2503 ) {
2504 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2505 }
2506
2507 break;
2508
2509 case $targetClass->subClasses:
2510 // If it might be a subtype, it can not be lazy. There isn't even
2511 // a way to solve this with deferred eager loading, which means putting
2512 // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2513 $newValue = $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity, $associatedId);
2514 break;
2515
2516 default:
2517 $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId);
2518
2519 switch (true) {
2520 // We are negating the condition here. Other cases will assume it is valid!
2521 case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER:
2522 $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId);
2523 $this->registerManaged($newValue, $associatedId, []);
2524 break;
2525
2526 // Deferred eager load only works for single identifier classes
2527 case isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2528 $hints[self::HINT_DEFEREAGERLOAD] &&
2529 ! $targetClass->isIdentifierComposite:
2530 // TODO: Is there a faster approach?
2531 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
2532
2533 $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId);
2534 $this->registerManaged($newValue, $associatedId, []);
2535 break;
2536
2537 default:
2538 // TODO: This is very imperformant, ignore it?
2539 $newValue = $this->em->find($assoc->targetEntity, $normalizedAssociatedId);
2540 break;
2541 }
2542 }
2543
2544 $this->originalEntityData[$oid][$field] = $newValue;
2545 $class->reflFields[$field]->setValue($entity, $newValue);
2546
2547 if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) {
2548 $inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy];
2549 $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity);
2550 }
2551
2552 break;
2553
2554 default:
2555 assert($assoc->isToMany());
2556 // Ignore if its a cached collection
2557 if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2558 break;
2559 }
2560
2561 // use the given collection
2562 if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2563 $data[$field]->setOwner($entity, $assoc);
2564
2565 $class->reflFields[$field]->setValue($entity, $data[$field]);
2566 $this->originalEntityData[$oid][$field] = $data[$field];
2567
2568 break;
2569 }
2570
2571 // Inject collection
2572 $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
2573 $pColl->setOwner($entity, $assoc);
2574 $pColl->setInitialized(false);
2575
2576 $reflField = $class->reflFields[$field];
2577 $reflField->setValue($entity, $pColl);
2578
2579 if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
2580 $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
2581 if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) {
2582 $this->scheduleCollectionForBatchLoading($pColl, $class);
2583 } else {
2584 $this->loadCollection($pColl);
2585 $pColl->takeSnapshot();
2586 }
2587 }
2588
2589 $this->originalEntityData[$oid][$field] = $pColl;
2590 break;
2591 }
2592 }
2593
2594 // defer invoking of postLoad event to hydration complete step
2595 $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2596
2597 return $entity;
2598 }
2599
2600 public function triggerEagerLoads(): void
2601 {
2602 if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) {
2603 return;
2604 }
2605
2606 // avoid infinite recursion
2607 $eagerLoadingEntities = $this->eagerLoadingEntities;
2608 $this->eagerLoadingEntities = [];
2609
2610 foreach ($eagerLoadingEntities as $entityName => $ids) {
2611 if (! $ids) {
2612 continue;
2613 }
2614
2615 $class = $this->em->getClassMetadata($entityName);
2616 $batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize());
2617
2618 foreach ($batches as $batchedIds) {
2619 $this->getEntityPersister($entityName)->loadAll(
2620 array_combine($class->identifier, [$batchedIds]),
2621 );
2622 }
2623 }
2624
2625 $eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion
2626 $this->eagerLoadingCollections = [];
2627
2628 foreach ($eagerLoadingCollections as $group) {
2629 $this->eagerLoadCollections($group['items'], $group['mapping']);
2630 }
2631 }
2632
2633 /**
2634 * Load all data into the given collections, according to the specified mapping
2635 *
2636 * @param PersistentCollection[] $collections
2637 */
2638 private function eagerLoadCollections(array $collections, ToManyInverseSideMapping $mapping): void
2639 {
2640 $targetEntity = $mapping->targetEntity;
2641 $class = $this->em->getClassMetadata($mapping->sourceEntity);
2642 $mappedBy = $mapping->mappedBy;
2643
2644 $batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true);
2645
2646 foreach ($batches as $collectionBatch) {
2647 $entities = [];
2648
2649 foreach ($collectionBatch as $collection) {
2650 $entities[] = $collection->getOwner();
2651 }
2652
2653 $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy);
2654
2655 $targetClass = $this->em->getClassMetadata($targetEntity);
2656 $targetProperty = $targetClass->getReflectionProperty($mappedBy);
2657 assert($targetProperty !== null);
2658
2659 foreach ($found as $targetValue) {
2660 $sourceEntity = $targetProperty->getValue($targetValue);
2661
2662 if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]->joinColumns)) {
2663 // case where the hydration $targetValue itself has not yet fully completed, for example
2664 // in case a bi-directional association is being hydrated and deferring eager loading is
2665 // not possible due to subclassing.
2666 $data = $this->getOriginalEntityData($targetValue);
2667 $id = [];
2668 foreach ($targetClass->associationMappings[$mappedBy]->joinColumns as $joinColumn) {
2669 $id[] = $data[$joinColumn->name];
2670 }
2671 } else {
2672 $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
2673 }
2674
2675 $idHash = implode(' ', $id);
2676
2677 if ($mapping->indexBy !== null) {
2678 $indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy);
2679 assert($indexByProperty !== null);
2680 $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
2681 } else {
2682 $collectionBatch[$idHash]->add($targetValue);
2683 }
2684 }
2685 }
2686
2687 foreach ($collections as $association) {
2688 $association->setInitialized(true);
2689 $association->takeSnapshot();
2690 }
2691 }
2692
2693 /**
2694 * Initializes (loads) an uninitialized persistent collection of an entity.
2695 *
2696 * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2697 */
2698 public function loadCollection(PersistentCollection $collection): void
2699 {
2700 $assoc = $collection->getMapping();
2701 $persister = $this->getEntityPersister($assoc->targetEntity);
2702
2703 switch ($assoc->type()) {
2704 case ClassMetadata::ONE_TO_MANY:
2705 $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2706 break;
2707
2708 case ClassMetadata::MANY_TO_MANY:
2709 $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2710 break;
2711 }
2712
2713 $collection->setInitialized(true);
2714 }
2715
2716 /**
2717 * Schedule this collection for batch loading at the end of the UnitOfWork
2718 */
2719 private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void
2720 {
2721 $mapping = $collection->getMapping();
2722 $name = $mapping->sourceEntity . '#' . $mapping->fieldName;
2723
2724 if (! isset($this->eagerLoadingCollections[$name])) {
2725 $this->eagerLoadingCollections[$name] = [
2726 'items' => [],
2727 'mapping' => $mapping,
2728 ];
2729 }
2730
2731 $owner = $collection->getOwner();
2732 assert($owner !== null);
2733
2734 $id = $this->identifierFlattener->flattenIdentifier(
2735 $sourceClass,
2736 $sourceClass->getIdentifierValues($owner),
2737 );
2738 $idHash = implode(' ', $id);
2739
2740 $this->eagerLoadingCollections[$name]['items'][$idHash] = $collection;
2741 }
2742
2743 /**
2744 * Gets the identity map of the UnitOfWork.
2745 *
2746 * @psalm-return array<class-string, array<string, object>>
2747 */
2748 public function getIdentityMap(): array
2749 {
2750 return $this->identityMap;
2751 }
2752
2753 /**
2754 * Gets the original data of an entity. The original data is the data that was
2755 * present at the time the entity was reconstituted from the database.
2756 *
2757 * @psalm-return array<string, mixed>
2758 */
2759 public function getOriginalEntityData(object $entity): array
2760 {
2761 $oid = spl_object_id($entity);
2762
2763 return $this->originalEntityData[$oid] ?? [];
2764 }
2765
2766 /**
2767 * @param mixed[] $data
2768 *
2769 * @ignore
2770 */
2771 public function setOriginalEntityData(object $entity, array $data): void
2772 {
2773 $this->originalEntityData[spl_object_id($entity)] = $data;
2774 }
2775
2776 /**
2777 * INTERNAL:
2778 * Sets a property value of the original data array of an entity.
2779 *
2780 * @ignore
2781 */
2782 public function setOriginalEntityProperty(int $oid, string $property, mixed $value): void
2783 {
2784 $this->originalEntityData[$oid][$property] = $value;
2785 }
2786
2787 /**
2788 * Gets the identifier of an entity.
2789 * The returned value is always an array of identifier values. If the entity
2790 * has a composite identifier then the identifier values are in the same
2791 * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2792 *
2793 * @return mixed[] The identifier values.
2794 */
2795 public function getEntityIdentifier(object $entity): array
2796 {
2797 return $this->entityIdentifiers[spl_object_id($entity)]
2798 ?? throw EntityNotFoundException::noIdentifierFound(get_debug_type($entity));
2799 }
2800
2801 /**
2802 * Processes an entity instance to extract their identifier values.
2803 *
2804 * @return mixed A scalar value.
2805 *
2806 * @throws ORMInvalidArgumentException
2807 */
2808 public function getSingleIdentifierValue(object $entity): mixed
2809 {
2810 $class = $this->em->getClassMetadata($entity::class);
2811
2812 if ($class->isIdentifierComposite) {
2813 throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2814 }
2815
2816 $values = $this->isInIdentityMap($entity)
2817 ? $this->getEntityIdentifier($entity)
2818 : $class->getIdentifierValues($entity);
2819
2820 return $values[$class->identifier[0]] ?? null;
2821 }
2822
2823 /**
2824 * Tries to find an entity with the given identifier in the identity map of
2825 * this UnitOfWork.
2826 *
2827 * @param mixed $id The entity identifier to look for.
2828 * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
2829 * @psalm-param class-string $rootClassName
2830 *
2831 * @return object|false Returns the entity with the specified identifier if it exists in
2832 * this UnitOfWork, FALSE otherwise.
2833 */
2834 public function tryGetById(mixed $id, string $rootClassName): object|false
2835 {
2836 $idHash = self::getIdHashByIdentifier((array) $id);
2837
2838 return $this->identityMap[$rootClassName][$idHash] ?? false;
2839 }
2840
2841 /**
2842 * Schedules an entity for dirty-checking at commit-time.
2843 *
2844 * @todo Rename: scheduleForSynchronization
2845 */
2846 public function scheduleForDirtyCheck(object $entity): void
2847 {
2848 $rootClassName = $this->em->getClassMetadata($entity::class)->rootEntityName;
2849
2850 $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
2851 }
2852
2853 /**
2854 * Checks whether the UnitOfWork has any pending insertions.
2855 */
2856 public function hasPendingInsertions(): bool
2857 {
2858 return ! empty($this->entityInsertions);
2859 }
2860
2861 /**
2862 * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2863 * number of entities in the identity map.
2864 */
2865 public function size(): int
2866 {
2867 return array_sum(array_map('count', $this->identityMap));
2868 }
2869
2870 /**
2871 * Gets the EntityPersister for an Entity.
2872 *
2873 * @psalm-param class-string $entityName
2874 */
2875 public function getEntityPersister(string $entityName): EntityPersister
2876 {
2877 if (isset($this->persisters[$entityName])) {
2878 return $this->persisters[$entityName];
2879 }
2880
2881 $class = $this->em->getClassMetadata($entityName);
2882
2883 $persister = match (true) {
2884 $class->isInheritanceTypeNone() => new BasicEntityPersister($this->em, $class),
2885 $class->isInheritanceTypeSingleTable() => new SingleTablePersister($this->em, $class),
2886 $class->isInheritanceTypeJoined() => new JoinedSubclassPersister($this->em, $class),
2887 default => throw new RuntimeException('No persister found for entity.'),
2888 };
2889
2890 if ($this->hasCache && $class->cache !== null) {
2891 $persister = $this->em->getConfiguration()
2892 ->getSecondLevelCacheConfiguration()
2893 ->getCacheFactory()
2894 ->buildCachedEntityPersister($this->em, $persister, $class);
2895 }
2896
2897 $this->persisters[$entityName] = $persister;
2898
2899 return $this->persisters[$entityName];
2900 }
2901
2902 /** Gets a collection persister for a collection-valued association. */
2903 public function getCollectionPersister(AssociationMapping $association): CollectionPersister
2904 {
2905 $role = isset($association->cache)
2906 ? $association->sourceEntity . '::' . $association->fieldName
2907 : $association->type();
2908
2909 if (isset($this->collectionPersisters[$role])) {
2910 return $this->collectionPersisters[$role];
2911 }
2912
2913 $persister = $association->type() === ClassMetadata::ONE_TO_MANY
2914 ? new OneToManyPersister($this->em)
2915 : new ManyToManyPersister($this->em);
2916
2917 if ($this->hasCache && isset($association->cache)) {
2918 $persister = $this->em->getConfiguration()
2919 ->getSecondLevelCacheConfiguration()
2920 ->getCacheFactory()
2921 ->buildCachedCollectionPersister($this->em, $persister, $association);
2922 }
2923
2924 $this->collectionPersisters[$role] = $persister;
2925
2926 return $this->collectionPersisters[$role];
2927 }
2928
2929 /**
2930 * INTERNAL:
2931 * Registers an entity as managed.
2932 *
2933 * @param mixed[] $id The identifier values.
2934 * @param mixed[] $data The original entity data.
2935 */
2936 public function registerManaged(object $entity, array $id, array $data): void
2937 {
2938 $oid = spl_object_id($entity);
2939
2940 $this->entityIdentifiers[$oid] = $id;
2941 $this->entityStates[$oid] = self::STATE_MANAGED;
2942 $this->originalEntityData[$oid] = $data;
2943
2944 $this->addToIdentityMap($entity);
2945 }
2946
2947 /* PropertyChangedListener implementation */
2948
2949 /**
2950 * Notifies this UnitOfWork of a property change in an entity.
2951 *
2952 * {@inheritDoc}
2953 */
2954 public function propertyChanged(object $sender, string $propertyName, mixed $oldValue, mixed $newValue): void
2955 {
2956 $oid = spl_object_id($sender);
2957 $class = $this->em->getClassMetadata($sender::class);
2958
2959 $isAssocField = isset($class->associationMappings[$propertyName]);
2960
2961 if (! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
2962 return; // ignore non-persistent fields
2963 }
2964
2965 // Update changeset and mark entity for synchronization
2966 $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2967
2968 if (! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
2969 $this->scheduleForDirtyCheck($sender);
2970 }
2971 }
2972
2973 /**
2974 * Gets the currently scheduled entity insertions in this UnitOfWork.
2975 *
2976 * @psalm-return array<int, object>
2977 */
2978 public function getScheduledEntityInsertions(): array
2979 {
2980 return $this->entityInsertions;
2981 }
2982
2983 /**
2984 * Gets the currently scheduled entity updates in this UnitOfWork.
2985 *
2986 * @psalm-return array<int, object>
2987 */
2988 public function getScheduledEntityUpdates(): array
2989 {
2990 return $this->entityUpdates;
2991 }
2992
2993 /**
2994 * Gets the currently scheduled entity deletions in this UnitOfWork.
2995 *
2996 * @psalm-return array<int, object>
2997 */
2998 public function getScheduledEntityDeletions(): array
2999 {
3000 return $this->entityDeletions;
3001 }
3002
3003 /**
3004 * Gets the currently scheduled complete collection deletions
3005 *
3006 * @psalm-return array<int, PersistentCollection<array-key, object>>
3007 */
3008 public function getScheduledCollectionDeletions(): array
3009 {
3010 return $this->collectionDeletions;
3011 }
3012
3013 /**
3014 * Gets the currently scheduled collection inserts, updates and deletes.
3015 *
3016 * @psalm-return array<int, PersistentCollection<array-key, object>>
3017 */
3018 public function getScheduledCollectionUpdates(): array
3019 {
3020 return $this->collectionUpdates;
3021 }
3022
3023 /**
3024 * Helper method to initialize a lazy loading proxy or persistent collection.
3025 */
3026 public function initializeObject(object $obj): void
3027 {
3028 if ($obj instanceof InternalProxy) {
3029 $obj->__load();
3030
3031 return;
3032 }
3033
3034 if ($obj instanceof PersistentCollection) {
3035 $obj->initialize();
3036 }
3037 }
3038
3039 /**
3040 * Tests if a value is an uninitialized entity.
3041 *
3042 * @psalm-assert-if-true InternalProxy $obj
3043 */
3044 public function isUninitializedObject(mixed $obj): bool
3045 {
3046 return $obj instanceof InternalProxy && ! $obj->__isInitialized();
3047 }
3048
3049 /**
3050 * Helper method to show an object as string.
3051 */
3052 private static function objToStr(object $obj): string
3053 {
3054 return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj);
3055 }
3056
3057 /**
3058 * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3059 *
3060 * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3061 * on this object that might be necessary to perform a correct update.
3062 *
3063 * @throws ORMInvalidArgumentException
3064 */
3065 public function markReadOnly(object $object): void
3066 {
3067 if (! $this->isInIdentityMap($object)) {
3068 throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3069 }
3070
3071 $this->readOnlyObjects[spl_object_id($object)] = true;
3072 }
3073
3074 /**
3075 * Is this entity read only?
3076 *
3077 * @throws ORMInvalidArgumentException
3078 */
3079 public function isReadOnly(object $object): bool
3080 {
3081 return isset($this->readOnlyObjects[spl_object_id($object)]);
3082 }
3083
3084 /**
3085 * Perform whatever processing is encapsulated here after completion of the transaction.
3086 */
3087 private function afterTransactionComplete(): void
3088 {
3089 $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void {
3090 $persister->afterTransactionComplete();
3091 });
3092 }
3093
3094 /**
3095 * Perform whatever processing is encapsulated here after completion of the rolled-back.
3096 */
3097 private function afterTransactionRolledBack(): void
3098 {
3099 $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void {
3100 $persister->afterTransactionRolledBack();
3101 });
3102 }
3103
3104 /**
3105 * Performs an action after the transaction.
3106 */
3107 private function performCallbackOnCachedPersister(callable $callback): void
3108 {
3109 if (! $this->hasCache) {
3110 return;
3111 }
3112
3113 foreach ([...$this->persisters, ...$this->collectionPersisters] as $persister) {
3114 if ($persister instanceof CachedPersister) {
3115 $callback($persister);
3116 }
3117 }
3118 }
3119
3120 private function dispatchOnFlushEvent(): void
3121 {
3122 if ($this->evm->hasListeners(Events::onFlush)) {
3123 $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3124 }
3125 }
3126
3127 private function dispatchPostFlushEvent(): void
3128 {
3129 if ($this->evm->hasListeners(Events::postFlush)) {
3130 $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3131 }
3132 }
3133
3134 /**
3135 * Verifies if two given entities actually are the same based on identifier comparison
3136 */
3137 private function isIdentifierEquals(object $entity1, object $entity2): bool
3138 {
3139 if ($entity1 === $entity2) {
3140 return true;
3141 }
3142
3143 $class = $this->em->getClassMetadata($entity1::class);
3144
3145 if ($class !== $this->em->getClassMetadata($entity2::class)) {
3146 return false;
3147 }
3148
3149 $oid1 = spl_object_id($entity1);
3150 $oid2 = spl_object_id($entity2);
3151
3152 $id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3153 $id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3154
3155 return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2);
3156 }
3157
3158 /** @throws ORMInvalidArgumentException */
3159 private function assertThatThereAreNoUnintentionallyNonPersistedAssociations(): void
3160 {
3161 $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3162
3163 $this->nonCascadedNewDetectedEntities = [];
3164
3165 if ($entitiesNeedingCascadePersist) {
3166 throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3167 array_values($entitiesNeedingCascadePersist),
3168 );
3169 }
3170 }
3171
3172 /**
3173 * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3174 * Unit of work able to fire deferred events, related to loading events here.
3175 *
3176 * @internal should be called internally from object hydrators
3177 */
3178 public function hydrationComplete(): void
3179 {
3180 $this->hydrationCompleteHandler->hydrationComplete();
3181 }
3182
3183 /** @throws MappingException if the entity has more than a single identifier. */
3184 private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, mixed $identifierValue): mixed
3185 {
3186 return $this->em->getConnection()->convertToPHPValue(
3187 $identifierValue,
3188 $class->getTypeOfField($class->getSingleIdentifierFieldName()),
3189 );
3190 }
3191
3192 /**
3193 * Given a flat identifier, this method will produce another flat identifier, but with all
3194 * association fields that are mapped as identifiers replaced by entity references, recursively.
3195 *
3196 * @param mixed[] $flatIdentifier
3197 *
3198 * @return array<string, mixed>
3199 */
3200 private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier): array
3201 {
3202 $normalizedAssociatedId = [];
3203
3204 foreach ($targetClass->getIdentifierFieldNames() as $name) {
3205 if (! array_key_exists($name, $flatIdentifier)) {
3206 continue;
3207 }
3208
3209 if (! $targetClass->isSingleValuedAssociation($name)) {
3210 $normalizedAssociatedId[$name] = $flatIdentifier[$name];
3211 continue;
3212 }
3213
3214 $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name));
3215
3216 // Note: the ORM prevents using an entity with a composite identifier as an identifier association
3217 // therefore, reset($targetIdMetadata->identifier) is always correct
3218 $normalizedAssociatedId[$name] = $this->em->getReference(
3219 $targetIdMetadata->getName(),
3220 $this->normalizeIdentifier(
3221 $targetIdMetadata,
3222 [(string) reset($targetIdMetadata->identifier) => $flatIdentifier[$name]],
3223 ),
3224 );
3225 }
3226
3227 return $normalizedAssociatedId;
3228 }
3229
3230 /**
3231 * Assign a post-insert generated ID to an entity
3232 *
3233 * This is used by EntityPersisters after they inserted entities into the database.
3234 * It will place the assigned ID values in the entity's fields and start tracking
3235 * the entity in the identity map.
3236 */
3237 final public function assignPostInsertId(object $entity, mixed $generatedId): void
3238 {
3239 $class = $this->em->getClassMetadata($entity::class);
3240 $idField = $class->getSingleIdentifierFieldName();
3241 $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
3242 $oid = spl_object_id($entity);
3243
3244 $class->reflFields[$idField]->setValue($entity, $idValue);
3245
3246 $this->entityIdentifiers[$oid] = [$idField => $idValue];
3247 $this->entityStates[$oid] = self::STATE_MANAGED;
3248 $this->originalEntityData[$oid][$idField] = $idValue;
3249
3250 $this->addToIdentityMap($entity);
3251 }
3252}
diff --git a/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php b/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php
new file mode 100644
index 0000000..b682125
--- /dev/null
+++ b/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Utility;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\Persistence\Mapping\ClassMetadata;
9
10/** @internal This class exists only to avoid code duplication, do not reuse it externally */
11final class HierarchyDiscriminatorResolver
12{
13 private function __construct()
14 {
15 }
16
17 /**
18 * This method is needed to make INSTANCEOF work correctly with inheritance: if the class at hand has inheritance,
19 * it extracts all the discriminators from the child classes and returns them
20 *
21 * @return null[]
22 * @psalm-return array<array-key, null>
23 */
24 public static function resolveDiscriminatorsForClass(
25 ClassMetadata $rootClassMetadata,
26 EntityManagerInterface $entityManager,
27 ): array {
28 $hierarchyClasses = $rootClassMetadata->subClasses;
29 $hierarchyClasses[] = $rootClassMetadata->name;
30
31 $discriminators = [];
32
33 foreach ($hierarchyClasses as $class) {
34 $currentMetadata = $entityManager->getClassMetadata($class);
35 $currentDiscriminator = $currentMetadata->discriminatorValue;
36
37 if ($currentDiscriminator !== null) {
38 $discriminators[$currentDiscriminator] = null;
39 }
40 }
41
42 return $discriminators;
43 }
44}
diff --git a/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php b/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php
new file mode 100644
index 0000000..3792d33
--- /dev/null
+++ b/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php
@@ -0,0 +1,83 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Utility;
6
7use BackedEnum;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\UnitOfWork;
10use Doctrine\Persistence\Mapping\ClassMetadataFactory;
11
12use function assert;
13use function implode;
14use function is_a;
15
16/**
17 * The IdentifierFlattener utility now houses some of the identifier manipulation logic from unit of work, so that it
18 * can be re-used elsewhere.
19 */
20final class IdentifierFlattener
21{
22 /**
23 * Initializes a new IdentifierFlattener instance, bound to the given EntityManager.
24 */
25 public function __construct(
26 /**
27 * The UnitOfWork used to coordinate object-level transactions.
28 */
29 private readonly UnitOfWork $unitOfWork,
30 /**
31 * The metadata factory, used to retrieve the ORM metadata of entity classes.
32 */
33 private readonly ClassMetadataFactory $metadataFactory,
34 ) {
35 }
36
37 /**
38 * convert foreign identifiers into scalar foreign key values to avoid object to string conversion failures.
39 *
40 * @param mixed[] $id
41 *
42 * @return mixed[]
43 * @psalm-return array<string, mixed>
44 */
45 public function flattenIdentifier(ClassMetadata $class, array $id): array
46 {
47 $flatId = [];
48
49 foreach ($class->identifier as $field) {
50 if (isset($class->associationMappings[$field]) && isset($id[$field]) && is_a($id[$field], $class->associationMappings[$field]->targetEntity)) {
51 $targetClassMetadata = $this->metadataFactory->getMetadataFor(
52 $class->associationMappings[$field]->targetEntity,
53 );
54 assert($targetClassMetadata instanceof ClassMetadata);
55
56 if ($this->unitOfWork->isInIdentityMap($id[$field])) {
57 $associatedId = $this->flattenIdentifier($targetClassMetadata, $this->unitOfWork->getEntityIdentifier($id[$field]));
58 } else {
59 $associatedId = $this->flattenIdentifier($targetClassMetadata, $targetClassMetadata->getIdentifierValues($id[$field]));
60 }
61
62 $flatId[$field] = implode(' ', $associatedId);
63 } elseif (isset($class->associationMappings[$field])) {
64 assert($class->associationMappings[$field]->isToOneOwningSide());
65 $associatedId = [];
66
67 foreach ($class->associationMappings[$field]->joinColumns as $joinColumn) {
68 $associatedId[] = $id[$joinColumn->name];
69 }
70
71 $flatId[$field] = implode(' ', $associatedId);
72 } else {
73 if ($id[$field] instanceof BackedEnum) {
74 $flatId[$field] = $id[$field]->value;
75 } else {
76 $flatId[$field] = $id[$field];
77 }
78 }
79 }
80
81 return $flatId;
82 }
83}
diff --git a/vendor/doctrine/orm/src/Utility/LockSqlHelper.php b/vendor/doctrine/orm/src/Utility/LockSqlHelper.php
new file mode 100644
index 0000000..7d135eb
--- /dev/null
+++ b/vendor/doctrine/orm/src/Utility/LockSqlHelper.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Utility;
6
7use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Platforms\DB2Platform;
10use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
11use Doctrine\DBAL\Platforms\SQLitePlatform;
12use Doctrine\DBAL\Platforms\SQLServerPlatform;
13
14/** @internal */
15trait LockSqlHelper
16{
17 private function getReadLockSQL(AbstractPlatform $platform): string
18 {
19 return match (true) {
20 $platform instanceof AbstractMySQLPlatform => 'LOCK IN SHARE MODE',
21 $platform instanceof PostgreSQLPlatform => 'FOR SHARE',
22 default => $this->getWriteLockSQL($platform),
23 };
24 }
25
26 private function getWriteLockSQL(AbstractPlatform $platform): string
27 {
28 return match (true) {
29 $platform instanceof DB2Platform => 'WITH RR USE AND KEEP UPDATE LOCKS',
30 $platform instanceof SQLitePlatform,
31 $platform instanceof SQLServerPlatform => '',
32 default => 'FOR UPDATE',
33 };
34 }
35}
diff --git a/vendor/doctrine/orm/src/Utility/PersisterHelper.php b/vendor/doctrine/orm/src/Utility/PersisterHelper.php
new file mode 100644
index 0000000..76e9242
--- /dev/null
+++ b/vendor/doctrine/orm/src/Utility/PersisterHelper.php
@@ -0,0 +1,108 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Utility;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\Query\QueryException;
10use RuntimeException;
11
12use function sprintf;
13
14/**
15 * The PersisterHelper contains logic to infer binding types which is used in
16 * several persisters.
17 *
18 * @link www.doctrine-project.org
19 */
20class PersisterHelper
21{
22 /**
23 * @return list<string>
24 *
25 * @throws QueryException
26 */
27 public static function getTypeOfField(string $fieldName, ClassMetadata $class, EntityManagerInterface $em): array
28 {
29 if (isset($class->fieldMappings[$fieldName])) {
30 return [$class->fieldMappings[$fieldName]->type];
31 }
32
33 if (! isset($class->associationMappings[$fieldName])) {
34 return [];
35 }
36
37 $assoc = $class->associationMappings[$fieldName];
38
39 if (! $assoc->isOwningSide()) {
40 return self::getTypeOfField($assoc->mappedBy, $em->getClassMetadata($assoc->targetEntity), $em);
41 }
42
43 if ($assoc->isManyToManyOwningSide()) {
44 $joinData = $assoc->joinTable;
45 } else {
46 $joinData = $assoc;
47 }
48
49 $types = [];
50 $targetClass = $em->getClassMetadata($assoc->targetEntity);
51
52 foreach ($joinData->joinColumns as $joinColumn) {
53 $types[] = self::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $em);
54 }
55
56 return $types;
57 }
58
59 /** @throws RuntimeException */
60 public static function getTypeOfColumn(string $columnName, ClassMetadata $class, EntityManagerInterface $em): string
61 {
62 if (isset($class->fieldNames[$columnName])) {
63 $fieldName = $class->fieldNames[$columnName];
64
65 if (isset($class->fieldMappings[$fieldName])) {
66 return $class->fieldMappings[$fieldName]->type;
67 }
68 }
69
70 // iterate over to-one association mappings
71 foreach ($class->associationMappings as $assoc) {
72 if (! $assoc->isToOneOwningSide()) {
73 continue;
74 }
75
76 foreach ($assoc->joinColumns as $joinColumn) {
77 if ($joinColumn->name === $columnName) {
78 $targetColumnName = $joinColumn->referencedColumnName;
79 $targetClass = $em->getClassMetadata($assoc->targetEntity);
80
81 return self::getTypeOfColumn($targetColumnName, $targetClass, $em);
82 }
83 }
84 }
85
86 // iterate over to-many association mappings
87 foreach ($class->associationMappings as $assoc) {
88 if (! $assoc->isManyToManyOwningSide()) {
89 continue;
90 }
91
92 foreach ($assoc->joinTable->joinColumns as $joinColumn) {
93 if ($joinColumn->name === $columnName) {
94 $targetColumnName = $joinColumn->referencedColumnName;
95 $targetClass = $em->getClassMetadata($assoc->targetEntity);
96
97 return self::getTypeOfColumn($targetColumnName, $targetClass, $em);
98 }
99 }
100 }
101
102 throw new RuntimeException(sprintf(
103 'Could not resolve type of column "%s" of class "%s"',
104 $columnName,
105 $class->getName(),
106 ));
107 }
108}